Aller au contenu
Sylvain Joffraud l4w
Retour

z-index 10000 : menus MUI vides dans un Drawer

Un z-index à 10000 sur un drawer suffit à vider tous les menus Select MUI qu’il contient : les options sont bien dans le DOM, mais le menu, à z-index: 1300 par défaut, s’ouvre derrière le panneau blanc opaque. Dans le SaaS d’abonnements sur lequel je travaille (React 18.2, @mui/material 5.14.17), le symptôme remonté était double : des listes déroulantes sans aucune option, puis un écran où plus rien n’était cliquable jusqu’au rechargement. La cause : un commit qui avait monté le drawer de z-index: 2 à 10000 pour faire passer des boutons au-dessus d’un overlay.

Le symptôme : un Select vide, puis une UI figée

Dans le tunnel d’onboarding, on ouvre le drawer d’édition d’un compte marchand, on clique sur la liste « Forme juridique » : le menu s’ouvre, vide. Après ce clic, les champs texte du drawer ne prennent plus le focus.

Le « drawer » est un composant maison, ModalRight : un Modal MUI stylé en panneau latéral par une classe CSS.

Premier réflexe : vérifier les données. Les 5 options figuraient bien dans le DOM. Mais document.elementFromPoint() aux coordonnées de l’une d’elles renvoyait… un champ du drawer. Les options existaient, elles étaient simplement dessous.

La fausse piste : la fonctionnalité qui avait rendu le bug visible

Le ticket avait été attribué à une évolution récente qui affichait la liste « Forme juridique » sur toutes les fiches, et non plus pour certains types de comptes seulement. Elle en avait surtout fait le premier Select manipulé de l’onboarding. Le vrai coupable se trouvait dans l’historique du CSS du drawer :

 .modal__right {
   position: absolute;
   background-color: rgba(0, 0, 0, 0.4);
   height: 100%;
-  z-index: 2;
+  z-index: 10000;
 }

Pourquoi un z-index trop haut vide le menu Select MUI ?

Le menu d’un Select MUI est un Popover, donc un Modal, rendu dans un portal en fin de <body>. Le drawer aussi. Les deux sont frères dans le même contexte d’empilement, et l’échelle MUI est fixée dans @mui/material/styles/zIndex.js :

Élémentz-index
drawer MUI1200
modal MUI (dont Menu, Popover, Select)1300
snackbar MUI1400
tooltip MUI1500
Drawer maison après le commit10000

À 10000, le drawer passe au-dessus de tous les overlays MUI. Avant, la classe CSS écrasait déjà le 1300 natif du Modal, mais avec 2 : les menus passaient largement devant.

L’UI figée vient du même mécanisme. À l’ouverture, un Modal MUI pose aria-hidden="true" sur ses frères de <body> (le ModalManager de @mui/base) et active un focus-trap. Le menu, invisible, garde le focus ; son backdrop transparent, qui ferme le menu au clic, est lui aussi sous le drawer. Les clics tombent sur le drawer rendu inerte, et le menu ne se ferme jamais.

Un overlay caché derrière un autre n’est pas seulement invisible : s’il pose un focus-trap, il rend inerte tout ce qui le recouvre.

La rustine : surenchérir à 10001

Premier correctif, ciblé, dans le composant partagé SelectWithLabel :

<MuiSelect
  {...register(fieldName, { ...requiredProps })}
  // Le menu doit passer au-dessus des drawers (z-index 10000)
  MenuProps={{ style: { zIndex: 10001 } }}
/>

Le style inline permettait un test simple : expect(popoverRoot.style.zIndex).toBe('10001'). Un fichier _popper.css, présent depuis 2024 comme le z-index: 10000 de l’autre drawer maison (ModalCenter), forçait déjà les date pickers à 10001. Les 29 tests des formulaires concernés passaient.

Peu après, le même symptôme revenait sur un Select brut, hors SelectWithLabel, dans le drawer « Nouveau document ». Corriger champ par champ oubliait forcément des overlays.

Le vrai correctif : revenir à l’échelle MUI

Plutôt que de surenchérir sur chaque overlay, j’ai redescendu les drawers au niveau modal de MUI, via une variable CSS :

:root {
  /* Échelle alignée sur MUI (drawer 1200 / modal 1300 / snackbar 1400 / tooltip 1500) */
  --z-index-modal: 1300;
}

.modal__right,
.modal__center {
  z-index: var(--z-index-modal);
}

À z-index égal, l’ordre du DOM tranche : le menu, portalé après le drawer, s’affiche au-dessus. C’est le comportement standard d’un Dialog MUI qui contient un Select. Le hack MenuProps et _popper.css ont été supprimés, et le test inverse a été verrouillé : expect(popoverRoot.style.zIndex).toBe(''), pour empêcher la réintroduction d’une surcharge.

Vérification : 82 tests Jest verts sur les composants concernés, puis contrôle dans l’application avec elementFromPoint sur quatre types de champs dans des drawers (Select brut, SelectWithLabel, DatePicker, Autocomplete). Effet de bord bienvenu : les Snackbar et Tooltip MUI natifs, à 1400 et 1500, cachés derrière le 10000, repassaient au-dessus des drawers.

Même famille de bug sur une carte Leaflet : un edge marker caché sous les contrôles, présent mais intappable.

Lessons learned


Partager cet article :

Article précédent
ACL par IP du VPN : le split-tunnel sort par la box
Article suivant
Service worker et `/_astro/` : l’app figée au chargement