# Un focus qui ne revient jamais après la suppression d’un bloc dans l’éditeur

> Un bug de synchronisation entre useRef et useEffect laisse le focus clavier dans le vide après la suppression d'un bloc personnalisé dans l'éditeur de blocs.

- Auteur : WordPress Développement
- Publié le : 2021-12-14
- Mis à jour le : 2021-12-14
- Catégorie : Accessibilité
- URL : https://www.wpmoderne.fr/accessibilite/focus-perdu-suppression-bloc-editeur-useref/

## L’essentiel

- useRef seul ne déclenche jamais de nouveau rendu au changement de valeur
- useEffect doit surveiller une donnée qui change réellement de référence
- Un focus perdu redirige l'utilisateur clavier vers le body sans avertissement

`useRef(null)` semblait, à première vue, le bon outil pour garder une référence vers le bloc suivant après suppression d'un bloc personnalisé dans l'éditeur. Le bug qui en résultait, pourtant, n'apparaissait que dans un cas précis : la suppression du dernier bloc utilisant ce composant sur la page, un scénario suffisamment rare en test manuel pour être passé inaperçu pendant plusieurs semaines de développement.

## Symptôme

Un bloc personnalisé, développé pour afficher une citation mise en forme dans l'éditeur, propose un bouton de suppression accessible au clavier. En cliquant ou en activant ce bouton avec la touche Entrée, le bloc disparaît correctement de l'éditeur. Le problème survient juste après : le focus clavier ne se retrouve nulle part de précis. La touche Tab suivante déplace le focus vers un élément de la barre d'outils supérieure de l'éditeur, sans lien logique avec l'endroit où l'utilisateur travaillait quelques secondes plus tôt.

Pour un utilisateur clavier, ce comportement casse totalement la continuité de la tâche : après une suppression, il doit chercher où se trouve désormais le focus avant de pouvoir reprendre son travail, une perte de repère que les utilisateurs voyants au clavier remarquent immédiatement et que les utilisateurs de lecteurs d'écran ne perçoivent parfois même pas, faute d'annonce du changement.

## Diagnostic

Le composant utilisait une `ref` pour cibler le bloc suivant dans la liste, avec l'intention de lui donner le focus une fois le bloc courant supprimé.

```
function BlocCitation({ clientId, onSupprimer }) {
  const refBlocSuivant = useRef(null);

  useEffect(() => {
    // Cette dépendance vide ne surveille jamais le changement réel
  }, []);

  function gererSuppression() {
    onSupprimer(clientId);
    if (refBlocSuivant.current) {
      refBlocSuivant.current.focus();
    }
  }

  return (
    <div>
      <button onClick={gererSuppression}>Supprimer la citation</button>
    </div>
  );
}
```

Le problème tient à deux éléments combinés. D'abord, `refBlocSuivant.current` est évaluée au moment de l'appel à `gererSuppression`, avant que React n'ait retiré le bloc courant du DOM et recalculé la liste des blocs restants : la référence pointe donc encore vers un état antérieur, ou vers rien du tout si le bloc supprimé était le dernier de la page. Ensuite, le tableau de dépendances vide de `useEffect` signifie que cet effet ne s'exécute qu'une seule fois au montage du composant, il ne peut donc jamais réagir à la disparition du bloc suivant dans une liste qui change dynamiquement.

## Correctif

La correction déplace la logique de focus dans un effet qui surveille explicitement la liste des blocs, déclenché après que React a terminé de mettre à jour le DOM suite à la suppression.

```
function BlocCitation({ clientId, onSupprimer, blocsRestants }) {
  const zoneEditeurRef = useRef(null);

  useEffect(() => {
    if (blocsRestants.length === 0 && zoneEditeurRef.current) {
      zoneEditeurRef.current.focus();
    }
  }, [blocsRestants]);

  function gererSuppression() {
    onSupprimer(clientId);
  }

  return (
    <div ref={zoneEditeurRef} tabIndex={-1}>
      <button onClick={gererSuppression}>Supprimer la citation</button>
    </div>
  );
}
```

Le tableau de dépendances de `useEffect` surveille désormais `blocsRestants`, une donnée qui change réellement de référence à chaque suppression, ce qui déclenche l'effet au bon moment, après la mise à jour du DOM par React. Un conteneur parent reçoit un attribut `tabIndex={-1}`, ce qui le rend focalisable par script sans l'ajouter à l'ordre naturel de tabulation, une pratique courante pour offrir une cible de repli sûre quand aucun bloc suivant précis n'existe.

> L'essentiel à retenir : useRef seul ne déclenche jamais de nouveau rendu au changement de valeur ; useEffect doit surveiller une donnée qui change réellement de référence ; Un focus perdu redirige l'utilisateur clavier vers le body sans avertissement

## Prévention

- Ne jamais lire une `ref` immédiatement après une action qui modifie l'état d'une liste : la valeur lue correspond souvent à l'état d'avant le rendu suivant.
- Faire correspondre le tableau de dépendances de `useEffect` à ce que l'effet surveille réellement, jamais un tableau vide par habitude ou par souci de performance mal placé.
- Prévoir systématiquement une cible de repli pour le focus (le conteneur parent, la barre d'outils, le titre de la section) quand l'élément normalement ciblé peut ne plus exister après une suppression.
- Ajouter un test manuel spécifique à la suppression du dernier élément d'une liste, un scénario souvent oublié parce que moins fréquent en usage courant que la suppression d'un élément intermédiaire.

> Un repère utile pour toute équipe travaillant sur des composants d'éditeur : tester systématiquement la suppression du premier élément, d'un élément intermédiaire et du dernier élément d'une liste. Les trois cas produisent souvent des comportements de focus différents, et seul le dernier révèle généralement ce type de bug.

## En résumé

Ce bug ne touchait pas la logique de suppression du contenu elle-même, qui fonctionnait parfaitement : il touchait uniquement la synchronisation entre une référence DOM et le cycle de rendu de React. Une dépendance manquante dans un tableau de `useEffect`, un piège classique et bien documenté des hooks React, suffisait à laisser un utilisateur clavier sans repère après une action pourtant anodine.
