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 (en-US) : 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()` (en-US) 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 (en-US), 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 (en-US) 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` (en-US) 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` (en-US) 
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` (en-US) 
Expérimental

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

La propriété `transition-behavior` (en-US) 
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 (en-US) 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 (en-US) 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 :