Comment faire une modale avec React : l'approche "HTML first"
Par Romain GuillemotPublié le
Faites du HTML avant de faire du CSS, ou du JS… ou du React.
Au commencement était une modale
Cette histoire a commencé avec une modale. J’avais besoin d’une fenêtre modale dans un projet React. Pour rappel, voici une bonne définition de wikipedia:
Une fenêtre modale crée un mode qui désactive la fenêtre principale mais la garde visible, avec la fenêtre modale comme fenêtre enfant devant elle. Les utilisateurs doivent interagir avec la fenêtre modale avant de pouvoir revenir à l’application parente.
En utilisant React, cela peut prendre la forme suivante :
<Modal trigger={<button type="button">Cliquez moi</button>}>
Lorem ipsum dans une modale
</Modal>
Avec une première implémentation du composant Modal
:
function Modal ({ trigger, children }) {
const [isOpen, setOpen] = useState(false);
return (
<>
{React.cloneElement(trigger, {
onClick: () => setOpen(true)
})}
{isOpen && (
<div>
<button
type="button"
onClick={() => setOpen(false)}>
X
</button>
<div>{children}</div>
</div>
)}
</>
);
}
J’ai supprimé les noms de classe et le style pour me concentrer sur la logique de la modale et sa sémantique. C’est un premier problème ici : la sémantique.
La modale est composé d’un déclencheur (trigger) et d’un contenu (children). Sauf que le contenu n’est pas explicitement décrit comme un contenu de fenêtre modale. De plus, ce composant Modal
gère le déclencheur et le contenu via différents mécanismes :
- Le trigger est une prop, en attente d’un élément (un conteneur et un contenu : ici un
<button>
avec un texte “Cliquez moi”). - Le lorem ipsum est le contenu du composant, passé comme nœud de rendu (contenu uniquement : le composant
Modal
enveloppera le texte dans une<div>
).
Puis vinrent les sous-composants
Une version plus sémantique et cohérente pourrait être :
<Modale>
<Modal.Trigger>Cliquez-moi</Modal.Trigger>
<Modal.Window>
Lorem ipsum dans une modale
</Modal.Window>
</Modal>
Ici le déclencheur et la fenêtre sont au même niveau, tandis que le lorem ipsum est explicitement le contenu de la fenêtre modale. Cela peut être réalisé en déclarant des nouveaux composants Trigger
et Window
en tant que propriétés de Modal
. Ce sont des sous-composants de React. Quelque chose comme ça :
function Modal(/* ... */) {
/* ... */
}
function Trigger(/* ... */) {
/* ... */
}
Modal.Trigger = Trigger;
function Window(/* ... */) {
/* ... */
}
Modal.Window = Window;
Selon notre implémentation précédente, Trigger
et Window
devraient afficher les boutons d’ouverture/fermeture. Modal
est un conteneur et se contentera d’afficher ses enfants :
function Modal({ children }) {
const [isOpen, setOpen] = useState(false);
return (
<>
{children}
</>
);
}
function Trigger({ children }) {
/* ... */
return (
<button
type="button"
onClick={() => setOpen(true)}>
{children}
</button>
);
}
Modal.Trigger = Trigger;
function Window({ children }) {
/* ... */
retour isOpen && (
<div>
<button
type="button"
onClick={() => setOpen(false)}>
X
</button>
{children}
</div>
);
}
Modal.Window = Window;
Sauf que isOpen
et setOpen
font partie de l’état de Modal
. Ils doivent donc être passés à ses enfants. Un prop drilling complexe. Complexe car il faudra d’abord “parser” les enfants pour récupérer Trigger
et Window
… Prenons la solution de facilité avec l’API Context :
const ModalContext = createContext();
function Modal({ children }) {
const [isOpen, setOpen] = useState(false);
return (
<ModalContext.Provider value={ { isOpen, setOpen } }>
{children}
</ModalContext.Provider>
);
}
function Trigger({ children }) {
const { setOpen } = useContext(ModalContext);
return (
<button
type="button"
onClick={() => setOpen(true)}>
{children}
</button>
);
}
Modal.Trigger = Trigger;
function Window({ children }) {
const { isOpen, setOpen } = useContext(ModalContext);
retour isOpen && (
<div>
<button
type="button"
onClick={() => setOpen(false)}>
X
</button>
{children}
</div>
);
}
Modal.Window = Window;
Quelle beauté ! N’est-il pas ?
L’approche HTML first
C’est une beauté. Vraiment. Une telle beauté qu’elle a été ajoutée à HTML depuis des années. Un élément avec un état ouvert/fermé, déclenché par un enfant, et contrôlant l’affichage de son contenu. Voilà les balises <details>
et <summary>
. Elles permettent à notre Modal
de prendre une nouvelle forme :
function Modal({ children }) {
return <details>{children}</details>;
}
function Trigger({ children }) {
return <summary>{children}</summary>;
}
Modal.Trigger = Trigger;
function Window({ children }) {
return <div>{children}</div>;
}
Modal.Window = Window;
Une démo complète avec un peu de style est disponible ici : https://codepen.io/rocambille/pen/poaoKYm.
Parfois, nous voulons développer des idées. Et parfois, nous les voulons si fort que nous commençons à écrire du code. Utiliser du JS ou tout autre langage/outil/framework, car c’est ce que nous avons appris. Utiliser du CSS pur lorsque cela est possible.
Parfois, nous devrions faire du HTML avant de faire du CSS, ou du JS… ou du React. En utilisant une approche HTML first ;)