# createPortal déplace un menu hors du DOM naturel et casse l’ordre de tabulation

> Diagnostic d'un bug où un menu contextuel, porté ailleurs dans le DOM via createPortal, perd son ordre logique de focus pour les développeurs de blocs personnalisés.

- Auteur : WordPress Développement
- Publié le : 2022-01-25
- Mis à jour le : 2022-01-25
- Catégorie : Accessibilité
- URL : https://www.wpmoderne.fr/accessibilite/createportal-menu-casse-ordre-tabulation/

## L’essentiel

- createPortal change la position DOM sans changer la position visuelle
- L'ordre de tabulation suit le DOM, jamais l'affichage à l'écran
- Un menu positionné en fin de document rompt la continuité logique du clavier

`ReactDOM.createPortal(enfant, conteneurCible)` rend un élément enfant dans un nœud DOM différent de celui de son parent React, tout en conservant sa place dans l'arbre de composants React lui-même. Cette fonction, disponible depuis React 16, sert couramment à afficher des menus contextuels, des info-bulles ou des fenêtres flottantes en dehors des contraintes de positionnement ou de découpage (`overflow: hidden`) imposées par un conteneur parent. C'est exactement ce point fort qui a provoqué le bug décrit ici sur un bloc personnalisé développé pour l'éditeur.

## Symptôme

Le bloc en question affiche un bouton « Options » dans la barre d'outils du bloc, qui ouvre un menu contextuel avec plusieurs actions : dupliquer, déplacer, modifier un réglage avancé. Visuellement, le menu s'ouvre juste sous le bouton, exactement là où on l'attend. Mais en testant la navigation au clavier avec la touche Tab après ouverture du menu, le focus ne se déplace pas vers la première option du menu : il saute à un endroit totalement différent de la page, souvent tout en bas de l'éditeur, là où se trouvent d'autres éléments sans rapport visuel avec le menu ouvert.

## Diagnostic

Le composant du menu utilisait `createPortal` pour rendre son contenu directement dans un conteneur dédié, ajouté à la fin du `<body>`, une pratique courante pour éviter les problèmes d'empilement visuel (`z-index`) liés à des conteneurs parents positionnés.

```
function MenuOptionsBloc({ enfants, estOuvert }) {
  if (!estOuvert) return null;

  return ReactDOM.createPortal(
    <div className="menu-options-bloc">{enfants}</div>,
    document.getElementById('portails-editeur')
  );
}
```

Le problème ne vient pas de la fonction elle-même, qui fonctionne exactement comme documentée : elle déplace bien le nœud DOM du menu vers le conteneur cible, situé en fin de document. Le problème vient de l'ordre de tabulation du navigateur, qui suit strictement l'ordre des nœuds dans le DOM, jamais leur position visuelle à l'écran. Le menu, visuellement positionné sous le bouton qui l'ouvre, se trouve en réalité, dans l'arbre DOM final, à des centaines de nœuds de distance de ce bouton, puisqu'il a été déplacé vers un conteneur ajouté en toute fin de `<body>`.

Un utilisateur clavier qui presse Tab juste après avoir ouvert le menu se retrouve donc à naviguer vers l'élément suivant dans l'ordre du DOM à cet endroit précis du document, sans aucun rapport avec le menu qu'il vient d'ouvrir visuellement.

> L'essentiel à retenir : createPortal change la position DOM sans changer la position visuelle ; L'ordre de tabulation suit le DOM, jamais l'affichage à l'écran ; Un menu positionné en fin de document rompt la continuité logique du clavier

## Correctif

La correction consiste à déplacer explicitement le focus vers le premier élément interactif du menu au moment de son ouverture, plutôt que de compter sur l'ordre naturel de tabulation pour y parvenir seul.

```
function MenuOptionsBloc({ enfants, estOuvert }) {
  const refMenu = useRef(null);

  useEffect(() => {
    if (estOuvert && refMenu.current) {
      const premierElement = refMenu.current.querySelector(
        'button, [href], input, [tabindex]:not([tabindex="-1"])'
      );
      if (premierElement) premierElement.focus();
    }
  }, [estOuvert]);

  if (!estOuvert) return null;

  return ReactDOM.createPortal(
    <div className="menu-options-bloc" ref={refMenu}>{enfants}</div>,
    document.getElementById('portails-editeur')
  );
}
```

Un second ajustement complète la correction : un gestionnaire de touche Échap referme le menu et restaure le focus sur le bouton d'ouverture, en s'appuyant sur la même logique de restauration explicite déjà nécessaire pour toute fenêtre ou tout panneau déplacé hors de son emplacement naturel dans le DOM.

```
function fermerEtRestaurerFocus(refBouton, fermerMenu) {
  fermerMenu();
  if (refBouton.current) refBouton.current.focus();
}
```

## Prévention

- Considérer tout usage de `createPortal` comme un signal automatique de vigilance sur le focus : le déplacement DOM qu'il opère casse systématiquement la continuité visuelle et logique par défaut.
- Gérer le focus d'entrée et de sortie explicitement pour tout contenu porté ailleurs dans le DOM, sans jamais compter sur l'ordre naturel de tabulation pour compenser ce déplacement.
- Tester systématiquement au clavier tout composant utilisant `createPortal`, même quand son rendu visuel semble parfaitement correct.
- Documenter, dans le code du composant, la raison du recours à `createPortal` (contournement d'un `overflow: hidden`, gestion d'empilement) pour que la vigilance sur le focus reste associée à cette décision technique lors des futures relectures.

## Ce que ce cas ne couvre pas

Les popovers natifs du navigateur, une fonctionnalité distincte du portail React étudié ici, gèrent leur propre positionnement dans une couche d'affichage séparée du DOM classique, avec des comportements de focus qui leur sont propres. Leur cas ne sera pas traité dans ce diagnostic, centré uniquement sur les portails construits à la main avec l'API React.

> Un repère à transmettre à toute équipe de développement de blocs personnalisés : la position visuelle d'un élément à l'écran ne dit jamais rien de sa position réelle dans l'ordre de tabulation. Seul un test clavier direct révèle cette information.

## En résumé

`createPortal` fait exactement ce que sa documentation annonce : déplacer un nœud DOM tout en conservant sa place dans l'arbre de composants React. Ce déplacement, invisible à l'écran, a un effet direct et systématique sur l'ordre de tabulation du navigateur, un effet de bord que la documentation officielle ne met pas particulièrement en avant et que seul un test clavier manuel permet de détecter de façon fiable.
