Créer des modales avec React
Après avoir exploré les différences entre une modale et une boîte de dialogue dans notre précédent article, il est temps de plonger dans la pratique. 🐱👤
Implémentation d'une modale en React
Exemple de la modale que nous allons développer dans cet article :
Code de la modale, celui-ci sera détaillé par la suite :
// Modal.tsx
function Modal({children, isOpen, handleClose}) {
// 👇 L'usage de cet ref va particulièrement nous intéresser
const dialogRef = useRef<HTMLDialogElement>(null);
const close = () => {
dialogRef.current?.close();
};
useEffect(() => {
const dialog = dialogRef.current;
if (isOpen && !dialogRef.current?.open) {
dialog?.showModal(); // 👈 usage propre à l'élément <dialog>
} else {
dialog?.close(); // 👈 usage propre à l'élément <dialog>
}
}, [isOpen]);
return (
<dialog
ref={dialogRef}
// 👇 capture l'élément 'close' et mise à jour de l'état du composant
onClose={handleClose}
>
{children}
<button
type="button"
onClick={close}
title="close modal"
aria-label="close modal"
>
close
</button>
</dialog>
);
}
// App.tsx
function App() {
const [isOpen, setIsOpen] = useState(false);
const handleClose = (() => {
setIsOpen(false);
});
const handleClick = (() => {
setIsOpen(true);
});
return (
<div>
<Modal
isOpen={isOpen}
handleClose={handleClose}
>
{/* Le contenu de la modale */}
<header>
<h1>Hello world</h1>
<small>pour être original...</small>
</header>
</Modal>
<button
type="button"
onClick={handleClick}
>
Open modal
</button>
<p>{/* le petit Lorem Ipsum qui va bien 👌 */}</p>
</div>
);
}
Utilisation de l'élément<dialog>
et useRef
L'élément <dialog>
est une balise HTML permettant de créer des boîtes de dialogue modales avec une facilité déconcertante.
Il faut noter que cette balise bénéficie en outre d'un excellent support par les navigateurs modernes.
Pour créer une modale, il suffit d'encadrer le contenu de la modale avec le tag dialog.
<dialog>
<p>Contenu de la modale</p>
</dialog>
Par défaut, la modale n'est pas visible, pour modifier son état le navigateur met quelques fonctions à notre disposition :
dialog.show(); // affiche l'élément comme une boîte de dialogue
dialog.showModal(); // affiche l'élément comme une modale
dialog.close(); // masque l'élément quelque soit s a nature
J'utilise alternativement les termes de "modale" et de "boîte de dialogue". Si besoin, cet article vous aidera à comprendre les différences entre boîte de dialogue et modale !
Nous utilisons useRef
pour conserver une référence vers l'élément du DOM afin d'invoquer directement ces méthodes.
Contrôle de l'état d'ouverture / fermeture de la modale
La responsabilité relative à l'état d'affichage de la modale est partagée entre le composant Modal et son parent App.
Lorsque la propriété isOpen
change, cela déclenche le hook useEffect
lequel force l'élément dialog à rentrer dans l'état souhaité en invoquant soit la méthode showModal ou close.
Fausse bonne idée : gérer l'ouverture via css
Contrairement à d'autres solutions, ajouter ou retirer des classes css pour modifier l'affichage de la modale provoque des résultats assez mitigés.
Comme nous le verrons plus loin, la dialog embarque avec elle toute une logique d'accessibilité. Pour que le navigateur la gère au mieux, il est recommandé d'utiliser ces méthodes plutôt que de bricoler quelque chose de son côté 😁.
Synchroniser le DOM avec le contexte React
Si dialog.close() provoque la fermeture de la modale, cela ne concerne que la logique DOM. Pour synchroniser l'état de vos composants React, la solution consiste à écouter l'événement close
émis par la dialog lors de sa fermeture.
<dialog
ref={dialogRef}
// 👇 capture l'élément 'close' et invoque handleClose, lequel a pour responsabilité de mettre l'état du composant à jour
onClose={handleClose}
>
{/*...*/}
</dialog>
Donner du style à sa modale 😎
Deux choses intéressantes à noter :
- Le pseudo élément ::backdrop : il représente le layer séparant le reste de la page de la modale. Ce pseudo élément présente l'avantage de lutter contre le fléau des DOM trop complexes.
- La propriété css backdrop-filter : elle permet d'appliquer un filtre... sur le backdrop. 🤯
dialog::backdrop {
background: rgba(50, 20, 100, 0.2);
backdrop-filter: blur(2px);
}
Amélioration : Bloquer le scroll en arrière-plan
🛑 Stop ! Pas besoin de JS !
Si votre premier réflexe est de tenter de développer des scripts complexes pour bloquer les événements du navigateur... et bien vous commettriez la même erreur que moi ! 😱
Après des heures à développer un script robuste, j'ai finalement tout supprimé (sans regret !) en découvrant la ligne suivante :
html:has(dialog[open]) {
overflow: hidden;
}
Tout est là.
Support de la fonctionnalité "click outside"
Implémentation
// Modal.tsx
//...
function handleClickOutside(event) {
if (!dialogRef.current) {
return;
}
const box = dialogRef.current?.getBoundingClientRect();
// On calcule si le curseur est à l'extérieur de la boîte englobante de la modale
if (
event.pageX < box.left ||
event.pageX > box.right ||
event.pageY < (box.top + window.scrollY) ||
event.pageY > (box.bottom + window.scrollY)
) {
close();
}
}
// ...
return (
<dialog
ref={dialogRef}
onClose={handleClose}
// 👇 On ajoute le handler "click outside" directement sur l'élément dialog
onClick={handleClickOutside}
>
// ...
</dialog>
);
Brancher l'écouteur "click-outside" sur l'élément dialog
Non, non, ce n'est pas une erreur : le "click outside" est bel et bien déclenché lorsqu'on clique sur la dialog 🙃.
En effet, le pseudo élément ::backdrop recouvre l'intégralité de la page, et comme il n'est pas possible de sélectionner des pseudos élément via l'api DOM, le navigateur estime que l'événement click doit être envoyé à l'élément dialog.
C'est pourquoi on écrit un brin de JS dans handleClickOutside pour vérifier si la souris se trouve réellement sur notre élément ou pas.
Augmenter la réutilisabilité avec un custom hook
Implémenter la logique de mise à jour de l'état en fonction de l'état d'ouverture de la modale peut vite être répétitif et agaçant.
Bonne nouvelle, React étant fait pour être agréable à utiliser, une solution à notre problème existe : les custom hooks.
function useModal() {
const [isOpen, setIsOpen] = useState(false);
const closeModal = (() => {
setIsOpen(false);
});
const openModal = (() => {
setIsOpen(true);
});
return {
isOpen,
closeModal,
openModal
};
}
function App() {
const { isOpen, openModal, closeModal } = useModal();
return (
<div>
<Modal
isOpen={isOpen}
// 👇 on remplace handleClose par closeModal
handleClose={closeModal}
>
{/* Le contenu de la modale */}
<header>
<h1>Hello world</h1>
<small>pour être original...</small>
</header>
</Modal>
<button
type="button"
// 👇 on remplace handleClick par openModal
onClick={openModal}
>
Open modal
</button>
// ...
</div>
);
}
🛠 Précisions techniques, détails et conseils d'accessibilité
La balise <dialog>
permet de créer très facilement aussi bien une boîte de dialogue classique, qu'une modale, le tout en résolvant plusieurs contraintes d'accessibilités pour nos.
Par défaut, elle se voit attribuer le rôle aria (aria-role) dialog.
Les différences concernant l'accessibilité seront surtout marquées par l'emploi de show
ou de showModale
, c'est-à-dire selon le type de widget souhaité.
Boîte de dialogue classique
- S'ouvre avec la méthode show.
- Ne gère pas automatiquement le contenu inerte.
👉aria-hidden ou inert permettra d'indiquer aux outils d'assistance qu'un contenu n'est plus visible / utilisable. - Propriété aria-modal="false" (implicite et par défaut) indique que l'élément n'est pas modal.
- Ne se ferme pas automatiquement lorsqu'on appuie sur
Escape
. De plus, si l'on souhaite implémenter ce type de comportement, bien penser à ne fermer que la dernière boîte de dialogue lorsqu'on appuie surEscape
Les "vraies" modales
- S'ouvre avec la méthode showModal.
- Le contenu situé à l'extérieur de la modale est automatiquement considéré comme inerte.
- Propriété aria-modal="true" ajoutée implicitement à l'élément.
👉 Combinée aux rôles dialog et alertdialog, elle rend inutile l'utilisation de aria-hidden sur le contenu extérieur de la modale car aria-modal indique aux technologies d'assistance que ce contenu est inerte. - Se ferme automatiquement lorsqu'on appuie sur
Escape
.
Conclusion :
L'élément dialog
offre une alternative accessible et légère pour la création de modale. Facilement adaptable, elle constitue à mes yeux une bonne approche contrairement aux grosses librairies qui, en présentant quantité de fonctionnalités, pénalisent finalement la page et l'application d'un code trop complexe et d'un DOM trop large, sans parler des soucis de maintenabilité sur le long terme lorsqu'on a trop de dépendances externes.