Créez vos propres panneaux de contenu extensibles et rétractables

Publié: 2022-03-10
Résumé rapide ↬ Dans UI/UX, un modèle commun qui est nécessaire à maintes reprises est celui d'un simple panneau d'ouverture et de fermeture animé, ou "tiroir". Vous n'avez pas besoin d'une bibliothèque pour les créer. Avec quelques notions de base en HTML/CSS et JavaScript, nous allons apprendre à le faire nous-mêmes.

Jusqu'à présent, nous les avons appelés "panneaux d'ouverture et de fermeture", mais ils sont également décrits comme des panneaux d'extension, ou plus simplement, des panneaux extensibles.

Pour clarifier exactement de quoi nous parlons, rendez-vous sur cet exemple sur CodePen :

Tiroir facile à afficher/masquer (Multiples) par Ben Frain sur CodePen.

Tiroir facile à afficher/masquer (Multiples) par Ben Frain sur CodePen.

C'est ce que nous allons construire dans ce court tutoriel.

Du point de vue de la fonctionnalité, il existe plusieurs façons d'obtenir l'ouverture et la fermeture animées que nous recherchons. Chaque approche a ses propres avantages et inconvénients. Je vais partager les détails de ma méthode "go-to" en détail dans cet article. Considérons d'abord les approches possibles.

Approches

Il existe des variations sur ces techniques, mais d'une manière générale, les approches entrent dans l'une des trois catégories suivantes :

  1. Animez/transitionnez la height ou la hauteur max-height du contenu.
  2. Utilisez transform: translateY pour déplacer les éléments dans une nouvelle position, donnant l'illusion d'un panneau se fermant, puis restituez le DOM une fois la transformation terminée avec les éléments dans leur position finale.
  3. Utilisez une bibliothèque qui fait une combinaison/variation de 1 ou 2 !
Plus après saut! Continuez à lire ci-dessous ↓

Considérations de chaque approche

Du point de vue des performances, l'utilisation d'une transformation est plus efficace que l'animation ou la transition de la hauteur/hauteur maximale. Avec une transformation, les éléments en mouvement sont tramés et déplacés par le GPU. Il s'agit d'une opération facile et bon marché pour un GPU, de sorte que les performances ont tendance à être bien meilleures.

Les étapes de base lors de l'utilisation d'une approche de transformation sont :

  1. Obtenir la hauteur du contenu à replier.
  2. Déplacez le contenu et tout ce qui suit de la hauteur du contenu à réduire en utilisant transform: translateY(Xpx) . Opérez la transformation avec la transition de votre choix pour donner un effet visuel agréable.
  3. Utilisez JavaScript pour écouter l'événement transitionend . Quand il se déclenche, display: none le contenu et supprimez la transformation et tout devrait être au bon endroit.

Ça ne sonne pas trop mal, non ?

Cependant, il y a un certain nombre de considérations avec cette technique, donc j'ai tendance à l'éviter pour les implémentations occasionnelles à moins que les performances ne soient absolument cruciales.

Par exemple, avec l'approche transform: translateY , vous devez tenir compte de l' z-index des éléments. Par défaut, les éléments qui se transforment vers le haut se trouvent après l'élément déclencheur dans le DOM et apparaissent donc au-dessus des éléments qui les précèdent lorsqu'ils sont traduits vers le haut.

Vous devez également tenir compte du nombre de choses qui apparaissent après le contenu que vous souhaitez réduire dans le DOM. Si vous ne voulez pas un gros trou dans votre mise en page, vous trouverez peut-être plus facile d'utiliser JavaScript pour envelopper tout ce que vous voulez déplacer dans un élément conteneur et simplement le déplacer. Gérable mais nous venons d'introduire plus de complexité ! C'est cependant le genre d'approche que j'ai choisi pour déplacer les joueurs de haut en bas dans In / Out. Vous pouvez voir comment cela a été fait ici.

Pour des besoins plus occasionnels, j'ai tendance à opter pour la transition de la max-height du contenu. Cette approche n'est pas aussi performante qu'une transformation. La raison en est que le navigateur interpole la hauteur de l'élément qui s'effondre tout au long de la transition ; cela entraîne de nombreux calculs de mise en page qui ne sont pas aussi bon marché pour l'ordinateur hôte.

Cependant, cette approche gagne du point de vue de la simplicité. La récompense de subir le coup de calcul mentionné ci-dessus est que le re-flow DOM prend en charge la position et la géométrie de tout. Nous avons très peu de calculs à écrire et le JavaScript nécessaire pour bien réussir est relativement simple.

L'éléphant dans la pièce : détails et éléments de synthèse

Ceux qui ont une connaissance intime des éléments HTML sauront qu'il existe une solution HTML native à ce problème sous la forme de details et d'éléments summary . Voici quelques exemples de balisage :

 <details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>

Par défaut, les navigateurs proposent un petit triangle d'affichage à côté de l'élément de résumé ; cliquez sur le résumé et le contenu sous le résumé est révélé.

Super, hein ? Les détails prennent même en charge l'événement toggle en JavaScript afin que vous puissiez faire ce genre de chose pour effectuer différentes choses selon qu'il est ouvert ou fermé (ne vous inquiétez pas si ce type d'expression JavaScript semble étrange ; nous y reviendrons plus en détail détail sous peu):

 details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })

OK, je vais arrêter votre excitation juste là. Les détails et les éléments récapitulatifs ne s'animent pas. Pas par défaut et il n'est actuellement pas possible de les ouvrir et de les fermer avec du CSS et du JavaScript supplémentaires.

Si vous savez le contraire, j'aimerais qu'on me prouve que j'ai tort.

Malheureusement, comme nous avons besoin d'une esthétique d'ouverture et de fermeture, nous devrons retrousser nos manches et faire le travail le meilleur et le plus accessible possible avec les autres outils à notre disposition.

Bien, avec les nouvelles déprimantes à l'écart, continuons à faire en sorte que cela se produise.

Modèle de balisage

Le balisage de base ressemblera à ceci :

 <div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>

Nous avons un conteneur extérieur pour envelopper l'expandeur et le premier élément est le bouton qui sert de déclencheur à l'action. Remarquez l'attribut type dans le bouton ? J'inclus toujours que, par défaut, un bouton à l'intérieur d'un formulaire effectuera une soumission. Si vous perdez quelques heures à vous demander pourquoi votre formulaire ne fonctionne pas et que des boutons sont impliqués dans votre formulaire ; assurez-vous de vérifier l'attribut type !

L'élément suivant après le bouton est le tiroir de contenu lui-même ; tout ce que vous voulez cacher et montrer.

Pour donner vie aux choses, nous utiliserons des propriétés personnalisées CSS, des transitions CSS et un peu de JavaScript.

Logique de base

La logique de base est celle-ci :

  1. Laissez la page se charger, mesurez la hauteur du contenu.
  2. Définissez la hauteur du contenu sur le conteneur comme valeur d'une propriété personnalisée CSS.
  3. Masquez immédiatement le contenu en lui ajoutant un attribut aria-hidden: "true" . L'utilisation aria-hidden garantit que la technologie d'assistance sait que le contenu est également masqué.
  4. Câblez le CSS de sorte que la max-height de la classe de contenu corresponde à la valeur de la propriété personnalisée.
  5. En appuyant sur notre bouton de déclenchement, la propriété aria-hidden passe de vrai à faux, ce qui fait basculer la max-height du contenu entre 0 et la hauteur définie dans la propriété personnalisée. Une transition sur cette propriété donne le flair visuel - ajustez au goût !

Remarque : Maintenant, ce serait un cas simple de basculement d'une classe ou d'un attribut si max-height: auto était égal à la hauteur du contenu. Malheureusement, ce n'est pas le cas. Allez crier à ce sujet au W3C ici.

Voyons comment cette approche se manifeste dans le code. Les commentaires numérotés montrent les étapes logiques équivalentes ci-dessus dans le code.

Voici le Javascript :

 // Get the containing element const container = document.querySelector(".container"); // Get content const content = document.querySelector(".content"); // 1. Get height of content you want to show/hide const heightOfContent = content.getBoundingClientRect().height; // Get the trigger element const btn = document.querySelector(".trigger"); // 2. Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { document.documentElement.classList.add("height-is-set"); 3. content.setAttribute("aria-hidden", "true"); }, 0); btn.addEventListener("click", function(e) { container.setAttribute("data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true"); // 5. Toggle aria-hidden content.setAttribute("aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true"); })

Le CSC :

 .content { transition: max-height 0.2s; overflow: hidden; } .content[aria-hidden="true"] { max-height: 0; } // 4. Set height to value of custom property .content[aria-hidden="false"] { max-height: var(--containerHeight, 1000px); }

Points à noter

Qu'en est-il des tiroirs multiples ?

Lorsque vous avez un certain nombre de tiroirs ouverts et masqués sur une page, vous devrez tous les parcourir car ils seront probablement de tailles différentes.

Pour gérer cela, nous devrons faire un querySelectorAll pour obtenir tous les conteneurs, puis réexécuter votre paramètre de variables personnalisées pour chaque contenu dans un forEach .

Ce setTimeout

J'ai un setTimeout avec une durée de 0 avant de définir le conteneur à masquer. C'est sans doute inutile, mais je l'utilise comme une approche "ceinture et accolades" pour m'assurer que la page a été rendue en premier afin que les hauteurs du contenu soient disponibles pour être lues.

Ne le déclenchez que lorsque la page est prête

Si vous avez d'autres choses en cours, vous pouvez choisir d'envelopper votre code de tiroir dans une fonction qui s'initialise au chargement de la page. Par exemple, supposons que la fonction tiroir soit intégrée dans une fonction appelée initDrawers nous pourrions faire ceci :

 window.addEventListener("load", initDrawers);

En fait, nous ajouterons cela sous peu.

Attributs data-* supplémentaires sur le conteneur

Il y a un attribut de données sur le conteneur externe qui est également basculé. Ceci est ajouté au cas où quelque chose devrait changer avec la gâchette ou le conteneur lorsque le tiroir s'ouvre/se ferme. Par exemple, nous voulons peut-être changer la couleur de quelque chose ou révéler ou basculer une icône.

Valeur par défaut sur la propriété personnalisée

Il existe une valeur par défaut définie sur la propriété personnalisée dans CSS de 1000px . C'est le bit après la virgule à l'intérieur de la valeur : var(--containerHeight, 1000px) . Cela signifie que si le --containerHeight est foiré d'une manière ou d'une autre, vous devriez toujours avoir une transition décente. Vous pouvez évidemment définir ce qui convient à votre cas d'utilisation.

Pourquoi ne pas simplement utiliser une valeur par défaut de 100 000 pixels ?

Étant donné que max-height: auto ne fait pas de transition, vous vous demandez peut-être pourquoi vous n'optez pas simplement pour une hauteur définie d'une valeur supérieure à celle dont vous auriez jamais besoin. Par exemple, 10000000px ?

Le problème avec cette approche est qu'elle passera toujours de cette hauteur. Si la durée de votre transition est définie sur 1 seconde, la transition "parcourra" 1 000 000 pixels en une seconde. Si votre contenu ne fait que 50 pixels de haut, vous obtiendrez un effet d'ouverture/fermeture assez rapide !

Opérateur ternaire pour les bascules

Nous avons utilisé un opérateur ternaire à plusieurs reprises pour basculer les attributs. Certaines personnes les détestent mais moi et d'autres les aimons. Ils peuvent sembler un peu bizarres et un peu "code golf" au début, mais une fois que vous vous êtes habitué à la syntaxe, je pense qu'ils sont plus simples à lire qu'un if/else standard.

Pour les non-initiés, un opérateur ternaire est une forme condensée de if/else. Ils sont écrits de manière à ce que la chose à vérifier soit en premier, puis le ? sépare ce qu'il faut exécuter si la vérification est vraie, puis le : pour distinguer ce qui doit être exécuté si la vérification est fausse.

 isThisTrue ? doYesCode() : doNoCode();

Nos bascules d'attribut fonctionnent en vérifiant si un attribut est défini sur "true" et si c'est le cas, définissez-le sur "false" , sinon, définissez-le sur "true" .

Que se passe-t-il lors du redimensionnement de la page ?

Si un utilisateur redimensionne la fenêtre du navigateur, il y a une forte probabilité que les hauteurs de notre contenu changent. Par conséquent, vous souhaiterez peut-être réexécuter la configuration de la hauteur des conteneurs dans ce scénario. Maintenant que nous envisageons de telles éventualités, il semble que le moment soit venu de refactoriser un peu les choses.

Nous pouvons créer une fonction pour définir les hauteurs et une autre fonction pour gérer les interactions. Ajoutez ensuite deux écouteurs sur la fenêtre ; un pour le chargement du document, comme mentionné ci-dessus, puis un autre pour écouter l'événement de redimensionnement.

Un petit plus A11Y

Il est possible d'ajouter une petite considération supplémentaire pour l'accessibilité en utilisant les attributs aria-expanded , aria-controls et aria-labelledby . Cela donnera une meilleure indication de la technologie assistée lorsque les tiroirs ont été ouverts/dépliés. Nous ajoutons aria-expanded="false" au balisage de notre bouton aux côtés aria-controls="IDofcontent" , où IDofcontent est la valeur d'un identifiant que nous ajoutons au conteneur de contenu.

Ensuite, nous utilisons un autre opérateur ternaire pour basculer l'attribut aria-expanded au clic dans le JavaScript.

Tous ensemble

Avec le chargement de la page, plusieurs tiroirs, le travail A11Y supplémentaire et la gestion des événements de redimensionnement, notre code JavaScript ressemble à ceci :

 var containers; function initDrawers() { // Get the containing elements containers = document.querySelectorAll(".container"); setHeights(); wireUpTriggers(); window.addEventListener("resize", setHeights); } window.addEventListener("load", initDrawers); function setHeights() { containers.forEach(container => { // Get content let content = container.querySelector(".content"); content.removeAttribute("aria-hidden"); // Height of content to show/hide let heightOfContent = content.getBoundingClientRect().height; // Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { container.classList.add("height-is-set"); content.setAttribute("aria-hidden", "true"); }, 0); }); } function wireUpTriggers() { containers.forEach(container => { // Get each trigger element let btn = container.querySelector(".trigger"); // Get content let content = container.querySelector(".content"); btn.addEventListener("click", () => { btn.setAttribute("aria-expanded", btn.getAttribute("aria-expanded") === "false" ? "true" : "false"); container.setAttribute( "data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true" ); content.setAttribute( "aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true" ); }); }); }

Vous pouvez également jouer avec sur CodePen ici :

Tiroir facile à afficher/masquer (Multiples) par Ben Frain sur CodePen.

Tiroir facile à afficher/masquer (Multiples) par Ben Frain sur CodePen.

Sommaire

Il est possible de continuer pendant un certain temps à affiner et à répondre à de plus en plus de situations, mais les mécanismes de base de la création d'un tiroir d'ouverture et de fermeture fiable pour votre contenu devraient maintenant être à votre portée. J'espère que vous êtes également conscient de certains des dangers. L'élément details ne peut pas être animé, max-height: auto ne fait pas ce que vous espériez, vous ne pouvez pas ajouter de manière fiable une valeur max-height massive et vous attendre à ce que tous les panneaux de contenu s'ouvrent comme prévu.

Pour réitérer notre approche ici : mesurez le conteneur, stockez sa hauteur en tant que propriété personnalisée CSS, masquez le contenu, puis utilisez une simple bascule pour basculer entre max-height de 0 et la hauteur que vous avez stockée dans la propriété personnalisée.

Ce n'est peut-être pas la méthode la plus performante, mais j'ai trouvé que, dans la plupart des situations, elle est parfaitement adéquate et bénéficie d'une mise en œuvre relativement simple.