Le scénario est presque toujours le même, à savoir une table de données dans un conteneur déroulant. Chaque ligne comporte un menu d'action, une petite liste déroulante avec quelques options, comme Modifier, Dupliquer et Supprimer. Vous le construisez, il semble fonctionner parfaitement de manière isolée, puis quelqu'un le met dans ce div déroulant et les choses s'effondrent. J'ai vu exactement ce bug dans trois bases de code différentes : le conteneur, la pile et le framework, tous différents. Le bug, cependant, est totalement identique. La liste déroulante est coupée au bord du conteneur. Ou bien il apparaît derrière un contenu qui devrait logiquement se trouver en dessous. Ou cela fonctionne bien jusqu'à ce que l'utilisateur fasse défiler, puis il dérive. Vous atteignez l'index z : 9999. Parfois, cela aide, mais d'autres fois, cela ne fait absolument rien. Cette incohérence est le premier indice que quelque chose de plus profond est en train de se produire. La raison pour laquelle cela revient sans cesse est que trois systèmes de navigateur distincts sont impliqués, et la plupart des développeurs comprennent chacun d'eux séparément, mais ne pensent jamais à ce qui se passe lorsque les trois entrent en collision : débordement, contextes d'empilement et blocs contenants.
Une fois que vous comprenez comment les trois interagissent, les modes de défaillance cessent de sembler aléatoires. En fait, ils deviennent prévisibles. Les trois choses qui causent réellement cela Examinons chacun de ces éléments en détail. Le problème du débordement Lorsque vous définissez overflow: Hidden, overflow: scroll ou overflow: auto sur un élément, le navigateur coupe tout ce qui s'étend au-delà de ses limites, y compris les descendants absolument positionnés. .scroll-conteneur { débordement : automatique ; hauteur : 300px ; /* Cela coupera la liste déroulante, point final */ }
.dropdown { position : absolue ; /* Peu importe -- toujours coupé par .scroll-container */ }
Cela m'a surpris la première fois que je l'ai rencontré. J'avais pris la position : absolue permettrait à un élément d'échapper au découpage d'un conteneur. Ce n’est pas le cas. En pratique, cela signifie qu'un menu positionné de manière absolue peut être coupé par n'importe quel ancêtre ayant une valeur de débordement non visible, même si cet ancêtre n'est pas le bloc contenant le menu. Le détourage et le positionnement sont des systèmes distincts. Il se trouve qu’ils entrent en collision d’une manière qui semble complètement aléatoire jusqu’à ce que vous compreniez les deux.
Voici un exemple React utilisant createPortal :
importer { createPortal } depuis 'react-dom' ; importer { useState, useEffect, useRef } depuis 'react' ;
function Dropdown({ancreRef, isOpen, enfants }) { const [position, setPosition] = useState({ haut : 0, gauche : 0 });
utiliserEffet(() => { if (isOpen && AnchorRef.current) { const rect = AnchorRef.current.getBoundingClientRect(); setPosition({ en haut : rect.bottom + window.scrollY, à gauche : rect.left + window.scrollX, }); } }, [isOpen, AnchorRef]);
si (!isOpen) renvoie null ;
retourner créerPortal(
Et bien sûr, nous ne pouvons pas ignorer l’accessibilité. Les éléments fixes qui apparaissent sur le contenu doivent toujours être accessibles au clavier. Si l’ordre de focus ne se déplace pas naturellement vers la liste déroulante fixe, vous devrez le gérer à l’aide de code. Il convient également de vérifier qu’il ne se trouve pas au-dessus d’un autre contenu interactif sans aucun moyen de le rejeter. Celui-là vous mord lors des tests de clavier. Positionnement de l'ancre CSS : où je pense que cela nous mène Le positionnement des ancres CSS est la direction qui m'intéresse le plus en ce moment. Je n’étais pas sûr de la quantité de spécifications réellement utilisable lorsque je l’ai examinée pour la première fois. Il vous permet de déclarer la relation entre une liste déroulante et son déclencheur directement en CSS, et le navigateur gère les coordonnées. .trigger { nom-d'ancre : --my-trigger ; }
.menu déroulant { position : absolue ; position-ancre : --my-trigger ; en haut : ancre (en bas) ; gauche : ancre (gauche) ; position-try-fallbacks : flip-block, flip-inline ; }
La propriété position-try-fallbacks est ce qui vaut la peine d'être utilisée par rapport à un calcul manuel. Le navigateur essaie des emplacements alternatifs avant d'abandonner, de sorte qu'une liste déroulante en bas de la fenêtre se retourne automatiquement vers le haut au lieu d'être coupée. La prise en charge des navigateurs est solide dans les navigateurs basés sur Chromium et se développe dans Safari. Firefox a besoin d'un polyfill. Le package @oddbird/css-anchor-positioning couvre la spécification principale. J'ai rencontré des cas extrêmes de mise en page qui nécessitaient des solutions de secours que je n'avais pas anticipées, alors traitez-le comme une amélioration progressive ou associez-le à unSolution de secours JavaScript pour Firefox. Bref, prometteur mais pas encore universel. Testez dans vos navigateurs cibles. Et en ce qui concerne l’accessibilité, déclarer une relation visuelle en CSS ne dit rien à l’arborescence d’accessibilité. aria-controls, aria-expanded, aria-haspopup — cette partie est toujours sur vous. Parfois, le correctif consiste simplement à déplacer l'élément Avant d'accéder à un portail ou d'effectuer des calculs de coordonnées, je pose toujours une question en premier : cette liste déroulante doit-elle réellement résider à l'intérieur du conteneur de défilement ? Si ce n’est pas le cas, déplacer le balisage vers un wrapper de niveau supérieur élimine complètement le problème, sans JavaScript ni calcul de coordonnées. Ce n’est pas toujours possible. Si le bouton et la liste déroulante sont encapsulés dans le même composant, déplacer l’un sans l’autre signifie repenser toute l’API. Mais quand vous pouvez le faire, il n’y a rien à déboguer. Le problème n’existe tout simplement pas. Ce que le CSS moderne ne résout toujours pas CSS a parcouru un long chemin ici, mais il y a encore des endroits où il vous laisse tomber. La position : les problèmes résolus et de transformation sont toujours là. C'est intentionnellement dans la spécification, ce qui signifie qu'aucune solution de contournement CSS n'existe. Si vous utilisez une bibliothèque d'animation qui enveloppe votre mise en page dans un élément transformé, vous avez à nouveau besoin de portails ou de positionnement d'ancrage. CSS Anchor Positioning est prometteur, mais nouveau. Comme mentionné précédemment, Firefox a toujours besoin d'un polyfill au moment où j'écris ces lignes. J'ai rencontré des cas extrêmes de mise en page qui nécessitaient des solutions de repli que je n'avais pas anticipées. Si vous avez aujourd’hui besoin d’un comportement cohérent sur tous les navigateurs, vous avez toujours recours à JavaScript pour les parties délicates. L'ajout pour lequel j'ai modifié mon flux de travail est l'API HTML Popover, désormais disponible dans tous les navigateurs modernes. Les éléments dotés de l'attribut popover s'affichent avant tout dans la couche supérieure du navigateur, sans qu'aucun positionnement JavaScript ne soit nécessaire.
La gestion des échappements, le rejet par clic à l'extérieur et une sémantique d'accessibilité solide sont disponibles gratuitement pour des éléments tels que les info-bulles, les widgets de divulgation et les superpositions simples. C’est le premier outil que j’utilise pour le moment. Cela dit, cela ne résout pas le positionnement. Cela résout la superposition. Vous avez toujours besoin du positionnement de l'ancre ou de JavaScript pour aligner un popover sur son déclencheur. L'API Popover gère la superposition. Le positionnement de l'ancre gère le placement. Utilisés ensemble, ils couvrent la plupart de ce que vous auriez pu faire auparavant dans une bibliothèque. Un guide de décision pour votre situation Après avoir traversé tout cela à la dure, voici ce que je pense réellement du choix maintenant.
Utilisez un portail. Je l'utiliserais lorsque le déclencheur réside au plus profond des conteneurs de défilement imbriqués. J'ai utilisé ce modèle pour les menus d'action de table et je l'ai associé à des contrôles de restauration du focus et d'accessibilité. C’est l’option la plus fiable, mais prévoyez du temps pour le câblage supplémentaire. Utilisez un positionnement fixe. Ceci est utile lorsque vous utilisez du JavaScript Vanilla ou un framework léger et que vous pouvez vérifier qu'aucun ancêtre n'applique de transformations ou de filtres. C’est simple à configurer et simple à déboguer, tant que cette contrainte est respectée. Utilisez CSS Anchor Positioning.Reach pour cela lorsque la prise en charge de votre navigateur le permet. Si la prise en charge de Firefox est requise, associez-la au polyfill @oddbird. C’est là que se dirige finalement la plate-forme et deviendra éventuellement votre approche de prédilection. Restructurez le DOM. Utilisez-le lorsque l'architecture le permet et que vous ne souhaitez aucune complexité d'exécution. Je pense que c’est probablement l’option la plus sous-estimée. Combinez des modèles. Faites cela lorsque vous souhaitez que le positionnement de l'ancre soit votre approche principale, associé à une solution de secours JavaScript pour les navigateurs non pris en charge. Ou un portail pour le placement DOM associé à getBoundingClientRect() pour la précision des coordonnées.
Conclusion J'avais l'habitude de traiter ce bug comme un problème ponctuel – quelque chose à corriger et à partir duquel passer. Mais une fois que je me suis assis avec lui assez longtemps pour comprendre les trois systèmes impliqués – le découpage de débordement, les contextes d’empilement et le confinement des blocs – cela a cessé de sembler aléatoire. Je pouvais consulter une liste déroulante brisée et retracer immédiatement quel ancêtre était responsable. Ce changement dans la façon dont je lis le DOM a été le véritable point à retenir. Il n’y a pas une seule bonne réponse. Ce que je recherchais dépendait de ce que je pouvais contrôler dans la base de code : des portails lorsque l'arbre des ancêtres était imprévisible ; positionnement fixe alors qu’il était propre et simple ; déplacer l'élément alors que rien ne m'arrêtait ; et positionnement de l'ancre maintenant,où je peux. Quel que soit votre choix, ne considérez pas l’accessibilité comme la dernière étape. D’après mon expérience, c’est exactement à ce moment-là qu’il est ignoré. Les relations ARIA, la gestion du focus, le comportement du clavier – tout cela n’est pas impeccable. Ils font partie de ce qui fait que la chose fonctionne réellement. Consultez le code source complet dans mon dépôt GitHub. Lectures complémentaires Voici les références auxquelles je revenais sans cesse en travaillant sur ceci :
Le contexte d'empilement (MDN) «Guide de positionnement des ancres CSS», Juan Diego Rodriguez « Premiers pas avec l'API Popover », Godstime Aburu Interface utilisateur flottante (floating-ui.com) Débordement CSS (MDN)