Créer des modales avec React
Tags: react, beginners, a11y, html
Published: 2023-06-10 09:05:04

---
description: "Découvrez comment créer facilement des modales accessibles en utilisant l'élément <dialog> et React."
cover_image: 'https://qphi.imgix.net/blog/articles/commons/react.png'
canonical_url: https://www.qphi.dev/posts/comment-creer-modale-react
---


Après avoir exploré [les différences entre une modale et une boîte de dialogue](https://www.qphi.dev/posts/difference-modale-dialog-box) dans notre précédent article, il est temps de plonger dans la pratique. 🐱‍👤

## Implémentation d'une modale en React

_Note : cette section utilise des iframe vers d'autres pages d'exemples disponibles vers mon blog. Celles-ci étant bloquées sur dev.to, il est possible de continuer la lecture de l'article sur mon [blog](https://www.qphi.dev/posts/difference-modale-dialog-box)._

Exemple de la modale que nous allons développer dans cet article :

<iframe src="/example/modal-with-dialog"></iframe>

Code de la modale, celui-ci sera détaillé par la suite :
```tsx
//  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>
    );
}
```

```tsx
// 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**.

```html
<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 :
```javascript
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](/posts/difference-modale-dialog-box.md) !*

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.  


```tsx
 <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. 🤯
```css
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 :

```css
html:has(dialog[open]) {
    overflow: hidden;
}
```

Tout est là.

## Support de la fonctionnalité "click outside"

### Implémentation
```tsx
// 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, <u>le pseudo élément **::backdrop** recouvre l'intégralité de la page</u>, 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.

```tsx
function useModal() {
    const [isOpen, setIsOpen] = useState(false);
    const closeModal = (() => {
        setIsOpen(false);
    });

    const openModal = (() => {
        setIsOpen(true);
    });

    return {
        isOpen,
        closeModal,
        openModal
    };
}
```

```tsx
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>
    );
}
```


<iframe src="/example/modal-with-dialog-and-click-outside"></iframe>

## 🛠 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**.<br/>
👉***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 sur `Escape`


### 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.<br/>
👉 *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.