Utiliser l'API Popover

L'API Popover offre un mécanisme standard, flexible et cohérent pour afficher des contenus sous forme de popover par-dessus les autres contenus d'une page. L'affichage des contenus en popovers peut être contrôlé de manière déclarative en utilisant des attributs HTML, ou via JavaScript. Cet article vous guide à travers les concepts et les fonctionnalités de l'API Popover, et vous montre comment l'utiliser.

Créer des popovers déclaratifs

Dans sa forme la plus simple, un popover est créé en ajoutant l'attribut popover à l'élément HTML qui va contenir le contenu du popover. Un id est également nécessaire pour pouvoir associer l'élément popover à un élément de contrôle.

html
<div id="my-popover" popover>Contenu du popover</div>

Note : Définir l'attribut popover sans valeur est équivalent à utiliser popover="auto".

Ajouter cet attribut masque l'élément dès le chargement de la page, comme si on lui appliquait la déclaration CSS display: none. Pour afficher/masquer le popover, il faut utiliser un ou plusieurs boutons de contrôle. Vous pouvez utiliser un élément <button> (ou <input> avec l'attribut type="button") en lui ajoutant l'attribut popovertarget avec la valeur de l'identifiant (attribut id) de l'élément popover à contrôler.

html
<button popovertarget="mypopover">Actionner le popover</button>
<div id="mypopover" popover>Contenu du popover</div>

Par défaut, l'élément de contrôle bascule l'état du popover entre affiché et masqué.

Vous pouvez modifier ce comportement en utilisant l'attribut popovertargetaction avec les valeurs "show", "hide" ou "toggle". Par exemple, pour créer des boutons différents qui affichent ou masquent le popover, vous pouvez utiliser le code suivant :

html
<button popovertarget="mypopover" popovertargetaction="show">
  Afficher le popover
</button>
<button popovertarget="mypopover" popovertargetaction="hide">
  Masquer le popover
</button>
<div id="mypopover" popover>Contenu du popover</div>

Vous pouvez voir ce code en action dans notre exemple de popover déclaratif (voir le code source).

Note : Si l'attribut popovertargetaction n'est pas défini, il vaudra "toggle" par défaut.

Quand un popover est affiché, la déclaration CSS display:none associée est retirée et il est placé dans la couche supérieure : de cette manière il est affiché par-dessus les autres éléments de la page.

L'état automatique, et la fermeture légère

Quand un élément popover a l'attribut popover ou popover="auto", il est considéré comme ayant l'état automatique. Deux comportements notables s'appliquent avec cet état automatique :

  • Le popover peut être fermé légèrement (light dismissed). Cela signifie que vous pouvez masquer le popover en cliquant en dehors de celui-ci.
  • Le popover peut être fermé à l'aide des mécanismes fournis par le navigateur, comme la touche Esc du clavier.
  • En général, un seul popover peut être affiché à la fois. Si un popover est déjà affiché, l'affichage d'un autre popover masquera le premier. La seule exception porte sur les popovers imbriqués les uns dans les autres. Lisez la section Popovers imbriqués pour plus d'informations.

Note : Les popovers avec l'état automatique sont également masqués lorsque les méthodes HTMLDialogElement.showModal() et Element.requestFullscreen() sont appelées sur un autre élément du document. Gardez à l'esprit qu'appeler ces méthodes sur un élément popover visible échouera dans la mesure ou ces méthodes n'ont pas de sens pour un popover visible. Cependant, vous pouvez appeler ces méthodes sur un élément avec l'attribut popover qui n'est pas encore visible.

L'état automatique est utile pour afficher un seul popover à la fois. Cela peut être utile lorsqu'on a plusieurs messages à afficher les uns à la suite des autres (plutôt que d'avoir un affichage confus et encombré), ou lorsqu'on affiche des messages de statut, où le dernier l'emportera de toute façon sur le statut précédent.

Pour observer ce comportement, vous pouvez consulter l'exemple popovers multiples (code source correspondant). Essayez de fermer les popovers affichés en cliquant en dehors de leur zone, et observez ce qui se passe si vous essayez d'en afficher plusieurs à la fois.

L'état manuel

Au lieu de l'état automatique, on peut utiliser l'état manuel, qu'on obtient en utilisant popover="manual" sur un élément popover :

html
<div id="mypopover" popover="manual">Contenu du popover</div>

Dans cet état :

  • Le popover ne peut pas être fermé en cliquant en dehors de sa zone (les boutons de contrôle déclaratif vus ci-avant fonctionnent toujours).
  • Plusieurs popovers indépendants peuvent être affichés en même temps.

Vous pouvez voir ce code en action dans notre exemple de popovers manuels (code source correspondant).

Afficher des popovers via JavaScript

Vous pouvez également afficher des popovers via une API JavaScript.

La propriété HTMLElement.popover permet de lire ou de définir la valeur de l'attribut popover. Elle peut être utilisée pour créer un popover en JavaScript, ou servir à la détection de fonctionnalité.

js
function supportsPopovers() {
  return HTMLElement.prototype.hasOwnProperty("popover");
}

De même :

HTMLButtonElement.popoverTargetElement et HTMLInputElement.popoverTargetElement

Permettent de connaître ou définir la valeur de l'attribut popovertarget, ce qui permet de créer des boutons de contrôle. Notez que la valeur de cette propriété est une référence vers l'élément popover.

HTMLButtonElement.popoverTargetAction et HTMLInputElement.popoverTargetAction

Permettent de connaître ou définir la valeur de l'attribut popovertargetaction, ce qui permet de spécifier l'action à effectuer sur l'élément popover contrôlé par le bouton.

En utilisant ces trois propriétés, vous pouvez créer un bouton de contrôle et un élément popover en JavaScript :

js
const popover = document.getElementById("mypopover");
const toggleBtn = document.getElementById("toggleBtn");

const keyboardHelpPara = document.getElementById("keyboard-help-para");

const popoverSupported = supportsPopover();

if (popoverSupported) {
  popover.popover = "auto";
  toggleBtn.popoverTargetElement = popover;
  toggleBtn.popoverTargetAction = "toggle";
} else {
  toggleBtn.style.display = "none";
}

Vous disposez également de plusieurs méthodes pour afficher ou masquer un popover :

HTMLElement.showPopover()

Affiche un popover.

HTMLElement.hidePopover()

Masque un popover.

HTMLElement.togglePopover()

Bascule un popover entre les états affiché et masqué.

Par exemple, vous pouvez vouloir contrôler l'affichage d'une bulle d'aide en :

  • Cliquant sur un bouton
  • En pressant une touche du clavier.

Le premier cas peut être obtenu grâce à la méthode HTML déclarative ou grâce à l'API JavaScript, comme illustré dans l'exemple précédent.

Pour le second cas, vous pouvez créer un gestionnaire d'évènement qui va écouter l'usage de deux touches, une pour afficher le popover et une pour le masquer :

js
document.addEventListener("keydown", (event) => {
  if (event.key === "h") {
    if (popover.matches(":popover-open")) {
      popover.hidePopover();
    }
  }

  if (event.key === "s") {
    if (!popover.matches(":popover-open")) {
      popover.showPopover();
    }
  }
});

Cet exemple utilise Element.matches() pour déterminer programmatiquement si un élément popover est affiché ou non. La pseudo-classe :popover-open ne correspond qu'aux popover ouverts. C'est important pour éviter les erreurs qui seront déclenchées si vous essayez d'afficher un popover déjà affiché ou de masquer un popover déjà masqué.

Une alternative consiste à programmer une seule touche pour afficher et masquer le popover, comme ceci :

js
document.addEventListener("keydown", (event) => {
  if (event.key === "h") {
    popover.togglePopover();
  }
});

Allez voir notre exemple d'interface d'aide (code source correspondant) pour voir les propriétés JavaScript des popovers, la détection de fonctionnalité et la méthode togglePopover() en action.

Masquer les popovers automatiquement avec un minuteur

Un autre scénario fréquent en JavaScript consiste à masquer un popover automatiquement après un certain temps. Par exemple, vous pouvez vouloir créer un système de notifications « toast » pour une application qui exécute de multiples actions en arrière plan (par exemple, du téléversement de fichiers multiples) et qui affichera une notification pour chaque action terminée. Pour cela, vous voulez utiliser des popovers manuels afin d'en afficher plusieurs en même temps et utiliser setTimeout() pour les supprimer. Une fonction pour gérer ce genre de popover pourrait ressembler à ceci :

js
function makeToast(result) {
  const popover = document.createElement("article");
  popover.popover = "manual";
  popover.classList.add("toast");
  popover.classList.add("newest");

  let msg;

  if (result === "success") {
    msg = "Action was successful!";
    popover.classList.add("success");
    successCount++;
  } else if (result === "failure") {
    msg = "Action failed!";
    popover.classList.add("failure");
    failCount++;
  } else {
    return;
  }

  popover.textContent = msg;
  document.body.appendChild(popover);
  popover.showPopover();

  updateCounter();

  setTimeout(() => {
    popover.hidePopover();
    popover.remove();
  }, 4000);
}

Vous pouvez également utiliser l'évènement beforetoggle pour réaliser des actions avant qu'un popover s'affiche ou ne disparaisse. Dans notre exemple, nous exécutons la fonction moveToastUp() pour déplacer les popovers vers le haut afin de faire de la place pour le nouveau popover :

js
popover.addEventListener("toggle", (event) => {
  if (event.newState === "open") {
    moveToastsUp();
  }
});

function moveToastsUp() {
  const toasts = document.querySelectorAll(".toast");

  toasts.forEach((toast) => {
    if (toast.classList.contains("newest")) {
      toast.style.bottom = `5px`;
      toast.classList.remove("newest");
    } else {
      const prevValue = toast.style.bottom.replace("px", "");
      const newValue = parseInt(prevValue) + 50;
      toast.style.bottom = `${newValue}px`;
    }
  });
}

Allez voir notre exemple de popover « toast » (code source correspondant) pour voir ce bout de code en action, avec des explications complètes sous forme de commentaires.

Popovers imbriqués

Il existe une exception à la règle indiquant qu'il ne peut y avoir qu'un seul popover affiché à la fois : les popovers imbriqués. Dans ce cas, plusieurs popovers peuvent être affichés en même temps, du fait de leur relation les uns par rapport aux autres. Ce comportement est pris en charge pour permettre certain cas d'utilisation comme les menus imbriqués.

Trois façons permettent de créer des popovers imbriqués :

  • Avec un descendant direct dans le DOM
    html
    <div popover>
      Parent
      <div popover>Enfant</div>
    </div>
    
  • En utilisant l'attribut popovertarget
    html
    <div popover>
      Parent
      <button popovertarget="toto">Cliquez ici</button>
    </div>
    
    <div id="toto" popover>Enfant</div>
    
  • En utilisant l'attribut anchor
    html
    <div popover id="toto">Parent</div>
    
    <div popover anchor="toto">Enfant</div>
    

Allez voir notre exemple de menu imbriqué (code source correspondant) pour avoir un exemple concret. Vous remarquerez que seuls quelques évènements ont été utilisés pour afficher et masquer le sous-menu au clavier et à la souris, ainsi que pour tout masquer lorsqu'une option est sélectionnée. Selon les méthodes de chargement de contenu que vous utilisez, SPA ou pages multiples, tous les évènements ne seront pas nécessairement utiles, ils ont été inclus dans cet exemple pour montrer comment les utiliser.

Mettre en forme les popovers

L'API Popover dispose de quelques fonctionnalités CSS qu'il est bon de connaître.

Pour ce qui est d'appliquer un style aux popovers eux-mêmes, vous pouvez les cibler avec le sélecteur d'attribut ([popover]) ou vous pouvez cibler les popovers ouverts avec la pseudo-classe :popover-open.

Dans les premiers exemples que nous avons donnés dans cet article, vous avez peut-être remarqué que les popovers s'affichaient au milieu de la zone d'affichage (viewport). Il s'agit du style par défaut, défini via la feuille de style du navigateur :

css
[popover] {
  position: fixed;
  inset: 0;
  width: fit-content;
  height: fit-content;
  margin: auto;
  border: solid;
  padding: 0.25em;
  overflow: auto;
  color: CanvasText;
  background-color: Canvas;
}

Pour surcharger le style par défaut et faire apparaître le popover autre part, vous devez surcharger ce style par défaut avec quelque chose dans ce genre :

css
:popover-open {
  width: 200px;
  height: 100px;
  position: absolute;
  inset: unset;
  bottom: 5px;
  right: 5px;
  margin: 0;
}

Vous pouvez voir un exemple de cette surcharge dans notre exemple de positionnement de popover (code source correspondant).

Le pseudo-élément ::backdrop est un élément plein écran placé directement derrière les éléments popovers dans la couche supérieure, ce qui permet d'ajouter des effets au contenu de la page derrière les popovers si nécessaire. Par exemple, vous pouvez vouloir flouter le contenu de la page derrière un popover pour aider l'utilisatrice ou l'utilisateur à se concentrer sur le contenu du popover :

css
::backdrop {
  backdrop-filter: blur(3px);
}

Allez voir notre exemple de popover avec arrière-plan flouté (code source correspondant) pour vous en faire une idée.

Animer les popovers

Les popovers sont mis en forme avec la déclaration display: none; quand ils sont fermés et avec display: block; quand ils sont ouverts, et sont respectivement retirés/ajoutés à la couche supérieure et à l'arbre d'accessibilité. En conséquence, pour que les popovers puissent être animés, la propriété display doit pouvoir être animée. Les navigateurs compatibles animent display avec une variation discrète. Concrètement, le navigateur passera de la valeur none à une autre valeur de manière à ce que l'animation affiche le contenu tout du long. Ainsi :

  • Quand display est animé de none à block (ou toute autre valeur visible de display), la valeur passera à block à 0% de la durée de l'animation, ce qui la rendra visible du début à la fin.
  • Quand display est animé de block (ou toute autre valeur visible de display) à none, la valeur passera à none à 100% de la durée de l'animation, ce qui la rendra visible du début à la fin.

Note : Quand on anime en utilisant les transitions CSS, la déclaration transition-behavior: allow-discrete doit être appliquée sur l'élément popover pour activer le comportement décrit ci-avant. Quand on anime avec les animations CSS, le comportement décrit ci-avant est activé par défaut, et il n'y a pas besoin de définir cette propriété.

Les transitions sur les popovers

Quand on anime un popover à l'aide des transitions CSS, les éléments suivant sont nécessaires :

Règle @ @starting-style Expérimental

Définissez un ensemble de valeurs de départ pour les propriétés appliquées au popover qui vont être la cible de la transformation. Ces valeurs seront utilisées lors de la première transition pour éviter des comportements inattendus. Par défaut, les transitions CSS ne sont possibles que quand une propriété change de valeur sur un élément visible. Elles ne s'appliquent pas lors du premier affichage d'un élément, ou quand display passe de none à une autre valeur.

La propriété display

Ajoutez display à la liste des transitions pour que le popover garde la valeur display: block (ou tout autre valeur visible de display) pendant toute la transition pour s'assurer que le contenu du popover soit visible tout du long.

La propriété overlay Expérimental

Ajoutez overlay à la liste des transitions pour s'assurer que le popover reste dans la couche supérieure pendant toute la transition pour s'assurer que le contenu du popover soit visible tout du long.

La propriété transition-behavior Expérimental

Définissez transition-behavior: allow-discrete; sur l'élément popover pour activer les transitions discrètes des propriétés display et overlay, ces deux propriétés n'étant pas animables par défaut.

Prenons un exemple pour voir ce que ça donne.

HTML

Le code HTML comprend un élément <div> transformé en popover avec l'attribut global popover, et un élément <button> qui contrôle l'affichage du popover avec l'attribut popovertarget.

html
<button popovertarget="mypopover">Afficher le popover</button>
<div popover="auto" id="mypopover">
  Je suis un Popover ! Je devrais être animé.
</div>

CSS

Les deux propriétés du popover que nous voulons transitionner sont opacity et transform. Nous voulons que le popover apparaisse/disparaisse avec une transition en fondu enchaîné tout en grossissant ou rapetissant horizontalement. Pour cela, nous définissons un état de départ pour ces propriétés pour le popover fermé (sélectionné avec le sélecteur d'attribut [popover]), et un état final correspondant au popover ouvert (sélectionné avec la pseudo-classe :popover-open). Nous utilisons également la propriété transition pour définir les propriétés à animer et la durée de la transition.

css
html {
  font-family: Arial, Helvetica, sans-serif;
}

/* Transition appliquée au popover */

[popover]:popover-open {
  opacity: 1;
  transform: scaleX(1);
}

[popover] {
  font-size: 1.2rem;
  padding: 10px;

  /* L'état final de l'animation de sortie */
  opacity: 0;
  transform: scaleX(0);

  transition:
    opacity 0.7s,
    transform 0.7s,
    overlay 0.7s allow-discrete,
    display 0.7s allow-discrete;
  /* Équivalent à
  transition: all 0.7s allow-discrete; */
}

/* Doit être placé après la règle [popover]:popover-open
   précédente pour prendre effet, car la spécificité est
   la même. */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scaleX(0);
  }
}

/* Transition pour l'ombre du popover */

[popover]::backdrop {
  background-color: rgb(0 0 0 / 0);
  transition:
    display 0.7s allow-discrete,
    overlay 0.7s allow-discrete,
    background-color 0.7s;
  /* Équivalent à
  transition: all 0.7s allow-discrete; */
}

[popover]:popover-open::backdrop {
  background-color: rgb(0 0 0 / 0.25);
}

/* Le sélecteur d'imbrication ne peut pas
   représenter les pseudo-éléments et on ne peut
   donc pas imbriquer cette règle starting-style. */

@starting-style {
  [popover]:popover-open::backdrop {
    background-color: rgb(0 0 0 / 0);
  }
}

Comme vu précédemment, nous avons également :

  • Défini un état de départ pour la transition dans un bloc @starting-style
  • Ajouté display à la liste des propriétés à transitionner de manière à ce que l'élément animé soit visible (avec display: block) pendant toute l'animation. Sans cela, la transition de fermeture ne serait pas visible, le popover disparaîtrait instantanément.
  • Ajouté overlay à la liste des propriétés à transitionner de manière à ce que l'élément popover reste dans la couche supérieure jusqu'à la fin de l'animation. L'impact de cet ajout n'est pas nécessairement perceptible pour des animations aussi simples que celle-ci. Cependant, dans certains cas plus complexes, le fait d'omettre cette propriété peut avoir pour conséquence de faire disparaitre l'élément avant la fin de l'animation de transition.
  • Ajouté allow-discrete aux transitions des propriétés display et overlay pour activer les transitions discrètes de ces propriétés.

Vous noterez que nous avons également défini une transition pour le pseudo-élément ::backdrop qui apparait derrière le popover quand il s'ouvre, provoquant un effet d'assombrissement du contenu de la page.

Résultat

Le code donne ce résultat :

Note : Parce que les popovers passent de display: none à display: block à chaque fois qu'ils apparaissent, le popover transitionne des styles définis dans @starting-style aux styles définis dans [popover]:popover-open à chaque fois qu'il apparait. Quand le popover se ferme, il transitionne des styles définis dans [popover]:popover-open aux styles définis dans [popover].

Il est possible que les styles de transition pour l'entrée et la sortie puissent être différents. Regarder notre démonstration d'utilisation des styles de départ pour voir un exemple.

Les animations sur les popovers

Quand on anime un popover avec les animations CSS, il y a un plusieurs différences à connaître :

  • On n'écrit pas de bloc @starting-style. Les valeurs initiales et finales de display sont fournies dans les étapes d'animation to et from.
  • Il n'est pas nécessaire d'activer explicitement les transitions discrètes. Il n'y a pas d'équivalent à allow-discrete pour les étapes d'animation.
  • Il n'est pas non plus nécessaire de définir overlay dans les étapes d'animation, c'est l'animation de display qui fait passer le popover de visible à masqué.

Prenons un exemple.

HTML

Le code HTML comprend un élément <div>, transformé en popover avec l'attribut global popover, et un élément <button> qui contrôle l'affichage du popover avec l'attribut popovertarget.

html
<button popovertarget="mypopover">Afficher le popover</button>
<div popover="auto" id="mypopover">
  Je suis un Popover ! Je devrais être animé.
</div>

CSS

Nous avons défini des étapes d'animation pour l'affichage et la disparition du popover, ainsi qu'une animation dédiée à l'apparition de l'arrière-plan. Notez qu'il n'est pas possible d'animer la disparition de l'arrière-plan : celui-ci étant retiré immédiatement du DOM quand le popover se ferme, il n'y a plus rien à animer.

css
html {
  font-family: Arial, Helvetica, sans-serif;
}

[popover] {
  font-size: 1.2rem;
  padding: 10px;
  animation: fade-out 0.7s ease-out;
}

[popover]:popover-open {
  animation: fade-in 0.7s ease-out;
}

[popover]:popover-open::backdrop {
  animation: backdrop-fade-in 0.7s ease-out forwards;
}

/* Étapes d'animation */

@keyframes fade-in {
  0% {
    opacity: 0;
    transform: scaleX(0);
  }

  100% {
    opacity: 1;
    transform: scaleX(1);
  }
}

@keyframes fade-out {
  0% {
    opacity: 1;
    transform: scaleX(1);
    /* display est nécessaire sur l'animation
       de fermeture pour que le popover soit
       visible jusqu'à la fin de l'animation. */
    display: block;
  }

  100% {
    opacity: 0;
    transform: scaleX(0);
    /* display: none n'est pas strictement
       nécessaire ici car c'est la valeur par
       défaut pour un popover fermer. Mais on
       l'inclut pour que le comportement soit
       clair. */
    display: none;
  }
}

@keyframes backdrop-fade-in {
  0% {
    background-color: rgb(0 0 0 / 0);
  }

  100% {
    background-color: rgb(0 0 0 / 0.25);
  }
}

Résultat

Le code donne ce résultat :