Présentation du problème
Voir en fin de ce billet le paragraphe « Améliorations ».
Demande pour un site Responsive avec un menu qui, en vue Mobile:
- S’ouvre en cliquant sur une icône Sandwich placée dans le bandeau
- S’ouvre immédiatement sous le bandeau (pas de off-canvas) et par-dessus le contenu de la page (comprendre: sans s’intercaler entre le bandeau et le contenu de page, donc sans pousser ce dernier vers le bas).
- Le menu doit être en accordéon
J’ai eu l’idée d’utiliser le composant Modal de Foundation 6 qui intègre une option full-screen et la possibilité de faire débuter la modale à x pixels du haut de la fenêtre (pour laisser le bandeau apparent). La modale de Foundation étant scrollable, je me suis dit « Bingo! ». Le code de ma modale ressemble à peu près à ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<p><button class="button" data-toggle="showNavAndSwitchlanguageForMobile">Menu</button></p> <div class="full reveal" id="showNavAndSwitchlanguageForMobile" data-reveal> <ul class="vertical menu accordion-menu" data-accordion-menu> <li> <a href="#">Item 1</a> <ul class="menu vertical nested"> <li><a href="#">Item 1A</a></li> <li><a href="#">Item 1B</a></li> </ul> </li> <li><a href="#">Item 2</a></li> </ul> </div> |
Problème: la modale semble mal calculer les hauteurs lorsqu’on utilise l’attribut optionnel data-v-offset
pour faire débuter la modale à x pixels du haut de la fenêtre. On ne peut pas scroller jusqu’en bas lorsque le contenu de celle-ci est plus haut que la hauteur totale de la fenêtre!!!
Mise en place de la solution
(optimisation) On stocke l’ID affecté à l’élément Modal dans une variable, car on va l’appeler à plusieurs reprises ensuite:
1 2 |
// Global vars var $mobileNavModal = $('#showNavAndSwitchlanguageForMobile'); |
On commence par récupérer la hauteur du bandeau (en utilisant la très pratique javascript Utility BOX fournie avec Foundation 6) pour l’affecter à la modale via l’attribut optionnel data-v-offset
du plugin Reveal (lui aussi fourni avec Foundation 6).
1 2 3 4 5 6 7 |
var mobileHeaderInfo = Foundation.Box.GetDimensions(document.getElementById('mobileHeader')); function printMobileHeaderHeight(){ $mobileNavModal.attr('data-v-offset', mobileHeaderInfo.height); } // Si la fenêtre est redimensionnée, on réinitialise la valeur. $('window').on('resize', printMobileHeaderHeight()); |
Seulement ensuite, on initialise le plugin Reveal de Foundation 6 (ou tous les plugins d’un coup dans notre exemple) :
1 |
$(document).foundation(); |
De base, la modale Foundation a une hauteur et une hauteur minimale fixées à 100%
ou 100vh
pour les navigateurs les plus récents. Ces styles sont affectés à l’élément Modal via CSS.
1 2 3 4 5 |
.reveal.full { height: 100%; height: 100vh; min-height: 100vh; } |
D’autres styles sont affectés à l’élément Modal, lors de son ouverture, via javascript (voir l’attribut style=""
) :
1 2 3 4 5 6 7 8 9 10 |
<div class="full reveal without-overlay" id="showNavAndSwitchlanguageForMobile" data-reveal="95bnsl-reveal" data-v-offset="65" role="dialog" aria-hidden="false" data-yeti-box="showNavAndSwitchlanguageForMobile" data-resize="showNavAndSwitchlanguageForMobile" style="display: block; top: 65px; left: 0px; margin: 0px;" tabindex="-1"> |
Pour éviter le bug nous concernant, il faut surcharger les valeurs de height
et de min-height
en les recalculant via javaScript en fonction de la valeur de data-v-offset
récupérée plus haut. On stocke les styles existants dans une variable elementStandardStyle
et on ajoute les nouvelles valeurs de hauteur via une variable elementCustomStyle
:
1 2 3 4 5 6 7 8 |
$mobileNavModal.on('open.zf.reveal', function(){ var elementStandardStyle = $(this).attr('style'); var elementCustomStyle = 'height: calc(100% - '+mobileHeaderInfo.height+'px); min-height: calc(100% - '+mobileHeaderInfo.height+'px);'; $(this) .attr('style', '') .attr('style', elementStandardStyle + elementCustomStyle); }); |
Optimisations d’ordre ergonomiques
Refermer la modale (le menu) au clic n’importe où d’autre sur l’écran
On commence par créer une fonction qui ferme la modale (cf. doc de Foundation).
1 2 3 |
function closeMobileNavModal(){ $mobileNavModal.foundation('close'); } |
Puis on exécute du code à l’événement open.zf.reveal
(une fois la modale est ouverte) :
- Pour une sélection d’éléments, la modale ne se refermera pas si on clique dessus.
- Pour tout le reste du document, on exécute la fonction
closeMobileNavModal()
qui referme la modale. - Le 2ème point n’est exécuté qu’une seule fois grâce au
$(this).off(e);
(sinon, on ne peut plus ré-ouvrir la modale).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$mobileNavModal.on('open.zf.reveal', function(){ $('#primaryNav-mobile, #mobileSwitchlanguage').on('click', function(e){ e.stopPropagation(); }); $(document).on('click', 'html', function(e){ closeMobileNavModal(); $(this).off(e); // [FIX][iPhone] A la 1ère fermeture de la modale, l'affichage ne revient pas // automatiquement en haut de page if ($('html.iphone').length == 1){ $(window).scrollTop(0); } }); }); |
ATTENTION!!! Sur iPhone, il existe un bug connu qui empêche le clic sur l’ensemble du $(document)
. Il faut impérativement ajouter la propriété cursor: pointer;
sur l’élément ciblé par le clic.
Dans l’exemple ci-dessous, je lance une détection de l’appareil servant à l’internaute et j’affiche une classe en conséquence sur l’élément html
via le plugin browser-detection.
1 2 3 |
html.iphone { cursor: pointer; } |
Améliorations (pas testé)
J’appelle deux fois l’événement open.zf.reveal
. On peut optimiser ça en ne l’appelant qu’une seule fois:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$mobileNavModal.on('open.zf.reveal', function(){ var elementStandardStyle = $(this).attr('style'); var elementCustomStyle = 'height: calc(100% - '+mobileHeaderInfo.height+'px); min-height: calc(100% - '+mobileHeaderInfo.height+'px);'; $(this) .attr('style', '') .attr('style', elementStandardStyle + elementCustomStyle); $('#primaryNav-mobile, #mobileSwitchlanguage').on('click', function(e){ e.stopPropagation(); }); $(document).on('click', 'html', function(e){ closeMobileNavModal(); $(this).off(e); // [FIX][iPhone] A la 1ère fermeture de la modale, l'affichage ne revient pas // automatiquement en haut de page if ($('html.iphone').length == 1){ $(window).scrollTop(0); } }); }); |
On peut conserver le menu ouvert sur la page en cours (à condition d’avoir les classes standard d’un menu Change .in-path, .current
) en ajoutant le code suivant:
Juste en dessous de l’initialisation des plug-ins Foundation ($(document).foundation();
) :
1 2 3 4 5 6 7 8 9 |
function openChildren($child){ if ($($child).attr('aria-expanded') === "false") { $($child).children('a').first().trigger('click'); } if ($($child).find('.inpath').length){ openChildren($($child).find('.inpath').first()); } } |
…puis à l’événement open.zf.reveal
(le if ($(primaryNav)
) :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$mobileNavModal.on('open.zf.reveal', function(){ var elementStandardStyle = $(this).attr('style'); var elementCustomStyle = 'height: calc(100% - '+mobileHeaderInfo.height+'px); min-height: calc(100% - '+mobileHeaderInfo.height+'px);'; var primaryNav = $('#primaryNav-mobile'); $(this) .attr('style', '') .attr('style', elementStandardStyle + elementCustomStyle); if ($(primaryNav).find('.inpath').first().length) { openChildren($(primaryNav).find('.inpath').first()); } }); |