Oihana Next UI - 0.48.0

Application logo

Modales

La famille des modales : la primitive et ses préréglages, les hooks CRUD, un pied sur mesure, et ce qu'un portail doit viser.

Exemples de modales avec le hook useModal

Modale simple

Libellés localisés

Aucune des trois modales ci-dessous ne reçoit de prop agree, disagree ni closeTitle. Chaque libellé — les deux boutons et le nom accessible de la croix de fermeture — est lu dans le bundle components.modal. Basculez la langue du labo et rouvrez-les : les libellés suivent.

Mode popover — léger, non bloquant (usePopover)

usePopover, sur demande : la modale passe par l'API Popover native du navigateur au lieu d'un dialog. Elle peut s'ouvrir DÉCLARATIVEMENT (un bouton avec popovertarget, sans JS) ou par useModal. Échap et un clic sur le fond la ferment. Elle NE bloque PAS la page — pour des panneaux légers, pas pour une confirmation bloquante.

Plein écran responsive (point de rupture)

Bascule d'une modale (avec suivi d'état)

Fermée

Modales d’alerte (un seul bouton)

Modales de confirmation (deux boutons)

Modale en plein écran

Responsive et placement

Largeur sur mesure

Options de comportement

Pied sur mesure

footerNode — pied collé en bas, contenu qui défile

Quand se servir de footerNode

Servez-vous de la prop footerNode quand le pied standard agree / disagree est trop rigide — typiquement pour un formulaire avec une ligne d'état, des boutons à soi, ou toute mise en page qui n'entre pas dans la rangée modal-action par défaut.

✅ Ce que ça apporte
  • Le pied toujours collé au bas de la modale
  • La zone de contenu qui défile seule, en douceur
  • L'en-tête qui reste en haut
  • Aucun !important à poser
  • Aucune tuyauterie modalBoxClassName à écrire
⚠️ Règles de priorité

Quand footerNode est donné, ces props sont IGNORÉES :

agree, disagree, agreeColor, disagreeColor, agreeIcon, disagreeIcon, showAgree, showDisagree, showFooter, footerReverse, footerClassName, footerOptions, onAgree, onCancel

Un console.warn est émis en développement si l’une d’elles est passée à côté.

Le mode standard (sans footerNode) ne change pas : le comportement de showFooter, avec sa rangée agree / disagree collée en bas, fonctionne exactement comme avant.
Avant / après

❌ Avant (recette à la main — 8 lignes, 5 marqueurs !)

<Modal
  contentClassName ="!overflow-hidden !p-0 flex flex-col flex-1 min-h-0"
  modalBoxClassName="!overflow-hidden flex flex-col"
  showFooter={false}>
  <div className="flex-1 min-h-0 overflow-y-auto ...">
    {form fields}
  </div>
  <div className="shrink-0 flex border-t bg-base-100 ...">
    {status + cancel + save}
  </div>
</Modal>

✅ Après — une prop, aucune surcharge

<Modal
  title="Edit profile"
  footerNode={<FormFooter ... />}
>
  <form className="flex flex-col gap-4">
    {form fields}
  </form>
</Modal>
// modal-box auto: flex flex-col
// content auto:   flex-1 min-h-0 overflow-y-auto

Formulaire dans une modale

Usage du hook useModal

const { modalRef, open, close, toggle, isOpen } = useModal() ;
// With callbacks
const { modalRef, open } = useModal({
  onOpen: () => console.log('Opened'),
  onClose: () => console.log('Closed'),
}) ;
<Button onClick={ open }>Open</Button>
<Modal ref={ modalRef }>Content</Modal>

Modales empilées (imbrication)

On peut ouvrir plusieurs modales l'une sur l'autre : le navigateur gère l'ordre d'empilement.

Tout fermer d’un coup

closeAllOpenDialogs() ferme chaque <dialog> ouvert du document, sans rien demander — ce qu’il faut avant un départ forcé, et seulement là. Les deux modales ci-dessous partent ensemble, depuis un bouton de la seconde.

Quatre préréglages qui demandent avant de lâcher

Une confirmation à retaper, une valeur montrée une seule fois, une question posée au passage, et une charge utile en lecture. Rien ici n’est détruit : les deux lignes sont des entrées d’un tableau.

ConfirmTypedModal

Une seule instance, repointée de ligne en ligne. Tapez un identifiant puis fermez par Échap ou par un clic à côté de la fenêtre, et rouvrez LA MÊME ligne : le champ est vide et le bouton verrouillé.

record-4817Premier enregistrement
record-9052Second enregistrement

SecretRevealModal

La valeur EST le déclencheur : il n’y a pas de drapeau d’ouverture. Rien ne la referme tant qu’elle n’est pas sauvegardée — essayez Échap. Copiez-la, puis attendez deux secondes : l’icône revient au presse-papiers et le bouton reste déverrouillé. Le toast est l’hôte qui réagit à un rappel, pas la bibliothèque.

PromptModal

Son montage EST la demande d’ouverture. Elle rend le texte sans ses espaces, et une chaîne vide quand rien n’a été tapé. Son champ grandit avec ce qu’on tape, par textAreaProps.

JsonViewerModal

Elle prend la valeur, pas une chaîne : la charge utile est mise en forme ici, et le bouton de copie est dans l’en-tête, là où un écran tactile peut l’atteindre. Elle prévient son hôte de la copie plutôt que de toaster — et l’hôte, ici, toaste.

InputModal — ouverture au focus

Cliquez dans le champ, ou servez-vous du bouton, pour ouvrir la modale.

Avec openOnFocus

Click the input field or the button to open

#3b82f6

Sans openOnFocus

Only the button opens the modal

Try clicking the input field - nothing happens. You must use the "Choose" button.

Quand se servir d’openOnFocus ?

✅ Bien pour :

  • Les sélecteurs de date
  • Les sélecteurs d’heure
  • Les sélecteurs de couleur
  • Les choix en un clic
  • Les champs en lecture seule

❌ À éviter pour :

  • Les formulaires complexes
  • Les choix en plusieurs étapes
  • Quand il faut taper
  • Les envois de fichier
  • Les champs modifiables
// ✅ Le bon patron, avec un état temporaire
const [value, setValue] = useState(initial) ;
const [tempValue, setTempValue] = useState(initial) ;
<InputModal
  value={value}  // ce qui est affiché
  onModalOpen={() => setTempValue(value)}
  onAgree={() => setValue(tempValue)}
>
  <input value={tempValue} onChange={setTempValue} />
</InputModal>

Autres options

Masquer le bouton d’action

Use showActionButton={false} to hide the button

onFocus personnalisé

Combine with your own focus handler

FormModal & ModalFooter

Modifiez le nom puis fermez : la question est posée. Enregistrez : le bouton attend, tout est verrouillé, puis la modale se ferme — sauf si le serveur refuse.

nom = AlphaonClose × 0

Les trois hooks CRUD

useAddModal, useEditModal, useRemoveModal

Fiches
4
Développeurs
2
Designers
1
NomCourrielTéléphoneRôleActions
AJ
Alice Johnson
alice@example.com555-0101Developer
BS
Bob Smith
bob@example.com555-0102Designer
CB
Charlie Brown
charlie@example.com555-0103Manager
DP
Diana Prince
diana@example.com555-0104Developer

useAddModal

valid: ✗

useEditModal

hasChanges: ✗
valid: ✗

useRemoveModal

ready: ✓

Confirmation asynchrone

La modale reste ouverte pendant l'action : bouton « En cours… », Annuler et la croix désactivés, Échap et le fond sans effet. Elle se ferme quand l'action réussit, et reste ouverte avec une erreur quand elle échoue.

Aucun archivage

Montée à la demande, ouverte au montage

Chaque bouton monte un composant qui ouvre sa propre modale depuis un effet de montage, sans second clic. Les quatre cartes ne diffèrent que par la façon dont la modale atteint le portail et dont on lui dit de s'ouvrir.

Enveloppée dans un Portal par l’appelant

Portal autour de Modal, et un showModal() sur le nœud.

Placée dans un portail par la modale elle-même

La prop portal sur Modal, et le même showModal().

Pilotée par useModal

open() depuis le hook, qui possède le nœud et ses écouteurs.

Déclarée avec openOnMount

useModal({ openOnMount : true }) et aucun effet dans le composant.

Le badge dit si le nœud existait quand l’effet a tourné. Une modale qui reste fermée avec « ref attachée » est un défaut différent d’une qui reste fermée avec « ref toujours nulle » : la seconde veut dire que l’effet a tourné avant que le nœud soit dans le document, et que l’appel optionnel l’a avalé.

Feuille montant du bas

Un panneau qui monte du bas de l'écran et se referme en le glissant vers le bas — ce qu'un menu ancré dans un coin ne peut pas être sur un appareil tenu à deux mains. Il ne porte aucun contenu : ce qui s'y pose appartient au produit.

Elle monte.

Glissez la feuille vers le bas, touchez à côté, pressez Échap, ou touchez une entrée : dans tous les cas elle redescend AVANT que le dialogue se ferme. Les entrées y arrivent par le close que la feuille passe à son contenu.

Bancs de non-régression

Les deux blocs qui suivent n'enseignent rien : ils existent pour qu'un défaut se voie. Un toast doit passer au-dessus d'une modale, et un panneau flottant doit dégager une page mise en plein écran — deux pièges du top layer qu'aucun z-index ne rattrape, donc deux choses à rouvrir après chaque changement.

Un toast au-dessus d’une modale

Vérifiez qu’un toast passe au-dessus du fond d’une modale. Le cas d’essai : le top layer natif du dialog contre le popover du ToastProvider.

Vertical
Horizontal
ActuelvAlign = bottomhAlign = end
Épreuve : un toast sous des modales empilées

Ces boutons déclenchent un toast D’ABORD, puis ouvrent une modale quelques centaines de millisecondes plus tard. Sans le MutationObserver du fournisseur, la modale volerait le sommet de la pile du top layer et masquerait le toast. Avec lui, le popover du toast est repromu dès que le nouveau dialog s’ouvre.

Essayez toutes les combinaisons (3 × 3 = 9 positions) : haut / milieu / bas × début / centre / fin. Chaque toast doit s’ancrer au coin, au bord ou au centre choisi — indépendamment de l’endroit où se trouve la modale.

Portal et le top layer du plein écran

Ouvrez chaque surface ci-dessous, passez en plein écran et ouvrez-les à nouveau. Les quatre doivent passer au-dessus de la page dans les deux états — un panneau qui s'ouvre à moitié derrière cette carte est un portail qui visait document.body pendant que le top layer était ailleurs.

cible du portail : document.body
Un popover sur un déclencheur
Une infobulle flottante
Survolez-moi
Une modale

Celle-ci n'a jamais été concernée : un <dialog> ouvert par showModal() entre de lui-même dans le top layer. Elle est là pour qu'une régression se voie à côté des autres plutôt que sur une autre page.

Ce qui se trouve dessous

Du remplissage, juste sous le déclencheur : un panneau ouvert doit couvrir ce texte, jamais glisser derrière. Il est haut exprès — laissez le popover ouvert et faites défiler : le panneau doit suivre son déclencheur plutôt que rester où il s'est ouvert.