13 octobre 2020

Focus: focus/blur

Un élément reçois le focus quand l’utilisateur clique dessus ou utilise la touche Tab du clavier. Il y a aussi un attribut HTML autofocus qui met le focus sur un élément par défaut lorsque la page charge et d’autres moyens d’obtenir un focus sont utilisées.

Avoir le focus sur un élément signifie généralement: “préparez-vous à accepter des données ici”, c’est donc le moment où nous pouvons exécuter le code pour initialiser la fonctionnalité requise.

Le moment où le focus est perdu (“blur”) peut être encore plus important. C’est quand l’utilisateur clique ailleurs ou appuie sur Tab, ou d’autre moyen, pour aller au champs de formulaire suivant.

Perdre le focus signifie généralement: “la donnée a été entrée”, nous pouvons donc exécuter le code pour la vérifier ou même pour la sauvegarder sur le serveur etc.

Il y a d’importantes particularités lorsque l’on travaille avec les événements de focsu. Nous ferons de notre mieux pour les couvrir plus loin.

Évènements focus/blur

L’évènement focus est appelé lors du focus, et blur – lorsque l’élément perds le focus

Utilisons les pour la validation d’un champ de saisie.

Dans l’example ci-dessous:

  • Le gestionnaire blur vérifie si l’adresse mail est entrée, et sinon – affiche une erreur.
  • Le gestionnaire focus masque le message d’erreur (au moment de blur le champ sera vérifié à nouveau):
<style>
  .invalid { border-color: red; }
  #error { color: red }
</style>

Entrez votre email: <input type="email" id="input">

<div id="error"></div>

<script>
input.onblur = function() {
  if (!input.value.includes('@')) { // pas une adresse email
    input.classList.add('invalid');
    error.innerHTML = 'Veuillez entrer une adresse email valide.'
  }
};

input.onfocus = function() {
  if (this.classList.contains('invalid')) {
    // retire l'erreur pour que l'utilisateur puisse entrer une nouvelle valeur
    this.classList.remove('invalid');
    error.innerHTML = "";
  }
};
</script>

Le HTML moderne nous permet de faire plusieurs validations en utilisant les attributs de champ de saisie: required, pattern, etc. Et parfois nous n’avons pas besoin de plus. JavaScript peut être utilisé lorsque nous avons besoin de plus de flexibilité. Nous pourrions aussi automatiquement envoyer les données modifiées sur le serveur si elles sont correctes.

Méthodes focus/blur

Les méthodes elem.focus() et elem.blur() mettent/retirent le focus d’un élément.

Par exemple, rendons impossible pour le visiteur de quitter le champ de saisie si la valeur est invalide:

<style>
  .error {
    background: red;
  }
</style>

Entrez votre email: <input type="email" id="input">
<input type="text" style="width:220px" placeholder="entrez une adresse email invalide et essayez de mettre le focus sur ce champ">

<script>
  input.onblur = function() {
    if (!this.value.includes('@')) { // pas une adresse email
      // affiche l'erreur
      this.classList.add("error");
      // ...et remet le focus
      input.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

Cela fonctionne sur tous les navigateurs à l’exception de Firefox (bug).

Si nous entrons quelque chose dans le champ de saisie puis essayons de Tab ou de cliquer en dehors du <input>, alors onblur remet le focus.

Veuillez noter que nous ne pouvons pas “empêcher la perte de focus” en appelant event.preventDefault() dans onblur, parce que onblur fonctionne après que l’élément ait perdu le focus.

Perte de focus initiée par le JavaScript

Une perte de focus peut avoir lieu pour plusieurs raisons.

L’une d’elle est lorsque le visiteur clique ailleurs. Mais JavaScript peut aussi la causer, par exemple:

  • Une alert déplace le focus sur elle-même, cela cause donc la perte de focus sur l’élément (évènement blur), et lorsque l’alert est fermée le focus reviens (évènement focus).
  • Si un élément est retiré du DOM, alors il entraine aussi la perte de focus. S’il est ré-ajouté plus tard le focus ne reviens pas.

Ces fonctionnalités amènent parfois les gestionnaires focus/blur à mal se conduire – se déclencher lorsque l’on n’en a pas besoin.

La meilleure recette est de faire attention lors de l’utilisation de ces événements. Si nous voulons suivre des pertes de focus initiées par l’utilisateur, alors nous devrions éviter de les causer nous-même.

Permettre de se focus sur n’importe quel élément: tabindex

Par défaut beaucoup d’éléments ne supportent pas le focus.

La liste change un peu selon les navigateurs, mais une chose est toujours vrai: le support de focus/blur est garanti pour les éléments avec lesquels le visiteur peut interagir: <button>, <input>, <select>, <a>, etc.

D’un autre côté, les éléments qui existent pour mettre quelque chose en forme, comme <div>, <span>, <table> – ne peuvent pas recevoir de focus par défaut. La méthode elem.focus() ne fonctionne pas sur eux, et les évènements focus/blur ne sont jamais déclenchés.

Cela peut être changé en utilisant l’attribut HTML tabindex.

N’importe quel élément peut recevoir le focus s’il a tabindex. La valeur de l’attribut est l’ordre de l’élément lorsque Tab (ou quelque chose dans le genre) est utilisé pour changer d’élément.

C’est-à-dire: si nous avons deux éléments, le premier ayant tabindex="1", et le deuxième ayant tabindex="2", alors appuyer sur Tab en étant sur le premier élément – déplace le focus sur le deuxième.

L’ordre de changement est: les éléments avec tabindex à 1 et plus sont en premier (dans l’ordre des tabindex), puis les éléments sans tabindex (ex. un <input> régulier).

Les éléments avec le même tabindex sont changés dans l’ordre du document (l’ordre par défaut).

Il y a deux valeurs spéciales:

  • tabindex="0" met l’élément parmi ceux qui ont tabindex. C’est-à-dire, lorsque l’on change d’élément, les éléments avec tabindex=0 vont après ceux avec tabindex ≥ 1.

    Généralement c’est utilisé pour permettre à un élément d’obtenir le focus tout en gardant l’ordre de changement par défaut. Pour intégrer un élément dans le formulaire de la même manière qu’un <input>.

  • tabindex="-1" permet seulement le focus par programmation. La touche Tab ignore ces éléments, mais la méthode elem.focus() fonctionne.

Par exemple, voici une liste. Cliquez sur le premier élément et appuyez sur Tab:

Cliquez sur le premier élément et appuyez sur Tabulation. Suivez l'ordre. Veuillez noter que plusieurs Tabulations à la suite peuvent déplacer le focus en dehors de l'iframe avec l'exemple.
<ul>
  <li tabindex="1">Un</li>
  <li tabindex="0">Zéro</li>
  <li tabindex="2">Deux</li>
  <li tabindex="-1">Moins un</li>
</ul>

<style>
  li { cursor: pointer; }
  :focus { outline: 1px dashed green; }
</style>

L’ordre est comme ceci: 1 - 2 - 0. Normalement, <li> ne supporte pas le focus, mais tabindex l’active, avec les évènements et le pseudo-sélecteur CSS :focus.

La propriété elem.tabIndex fonctionne aussi

Nous pouvons ajouter tabindex depuis le JavaScript en utilisant la propriété elem.tabIndex. Cela a le même effet.

Délégation: focusin/focusout

Les évènements focus et blur ne bubble pas.

Par exemple, nous ne pouvont pas ajouter onfocus sur le <form> pour le mettre en avant, comme ceci:

<!-- lors du focus sur le formulaire -- ajouter la classe -->
<form onfocus="this.className='focused'">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

L’exemple ci-dessus ne fonctionne pas, parce que lorsqu’un un user se focus sur un <input>, l’évènement focus ne se déclenche que sur ce champ de saisie. Il ne bubble pas. Donc form.onfocus ne se déclenche jamais.

Il y a deux solutions.

Premièrement, il y a une fonctionnalité historique: focus/blur ne bubble pas, mais il se propage durant la phase de capture.

Cela fonctionnera:

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  // ajouter le gestionnaire à la phase de capture (dernier argument true)
  form.addEventListener("focus", () => form.classList.add('focused'), true);
  form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>

Secondement, il y a les évènements focusin et focusout – exactement les mêmes que focus/blur, mais ils bubble.

Notez qu’ils doivent être assignés en utilisant elem.addEventListener, pas on<event>.

Voici donc une autre variante qui fonctionne:

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  form.addEventListener("focusin", () => form.classList.add('focused'));
  form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>

Résumé

Les évènements focus et blur se déclenchent lors du focus/perte de focus sur un élément.

Leur particularités sont:

  • Ils ne bubble pas. Possibilité d’utiliser la capture au lieu de focusin/focusout.
  • La plupart des éléments ne supportent pas le focus par défaut. Utilisez tabindex pour permettre à n’importe quoi d’avoir le focus.

L’élément ayant actuellement le focus est disponible en tant que document.activeElement.

Exercices

importance: 5

Créez une <div> qui se transforme en <textarea> lorsque l’on clique dessus.

La zone de texte permet de modifier le HTML présent dans la <div>.

Lorsque l’utilisateur appuie sur Enter ou perds le focus, la <textarea> se transforme à nouveau en <div>, et son contenu devient le HTML dans <div>.

Démo dans une nouvelle fenêtre

Open a sandbox for the task.

importance: 5

Rendre des cellules de table éditables au clique.

  • Au clique – la cellule doit devenir “éditable” (une zone de texte apparait à l’intérieur), nous pouvons changer le HTML. Il ne doit pas y avoir de redimensionnement, toute la géométrie doit rester la même.
  • Les boutons OK et CANCEL apparaissent sous la cellule pour terminer/annuler l’édition.
  • Seulement une cellule devrait être éditable à la fois. Pendant qu’un <td> est en “mode d’édition”, les cliques sur les autres cellules sont ignorés.
  • La table devrait avoir beaucoup de cellules. Utilisez la délégation d’évènements.

La démo:

Open a sandbox for the task.

  1. Au clique – remplacer innerHTML de la cellule par <textarea> avec les mêmes tailles et aucune bordure. Possibilité d’utiliser JavaScript ou CSS pour mettre les bonnes tailles.
  2. Mettre textarea.value a td.innerHTML.
  3. Mettre le focus sur la zone de texte.
  4. Afficher les boutons OK/CANCEL en dessous de la cellule, gèrent les cliques sur eux-mêmes.

Ouvrez la solution dans une sandbox.

importance: 4

Mettre le focus sur la souris. Puis utiliser les touches flèches pour la déplacer:

Démo dans une nouvelle fenêtre

P.S. Ne mettez de gestionnaire d’évènement que sur l’élément #mouse. P.P.S. Ne modifiez pas le HTML/CSS, l’approche devrait être générique et fonctionner avec n’importe quel élément.

Open a sandbox for the task.

Nous pouvons utiliser mouse.onclick pour gérer le clique et rendre la souris “déplaçable” avec position:fixed, puis mouse.onkeydown pour gérer les touches flèche.

Le seul problème est que keydown ne se déclenche que sur les éléments avec le focus. Donc nous avons besoin d’ajouter tabindex à l’élément. Comme nous n’avons pas le droit de modifier le HTML, nous pouvons utiliser la propriété mouse.tabIndex pour ceci.

P.S. Nous pouvons aussi remplacer mouse.onclick par mouse.onfocus.

Ouvrez la solution dans une sandbox.

Carte du tutoriel

Commentaires

lire ceci avant de commenter…
  • Si vous avez des améliorations à suggérer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de préciser.
  • Pour insérer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes -- enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen…)