Finestra di dialogo

Panoramica

Utilizza l'elemento <dialog> nativo per le finestre di dialogo modali e non modali. Fornisce la semantica di accessibilità, la gestione dello stato attivo, il rendering del livello superiore e la gestione di Esc che gli overlay personalizzati devono ricostruire manualmente.

Scegli il tipo di overlay

Scegli la prima regola corrispondente. In caso di dubbi, utilizza la Regola 1.

Regola 1: l'overlay interrompe l'utente e blocca la pagina (impostazione predefinita)

Utilizzalo per conferme, moduli, accesso, azioni distruttive e qualsiasi richiesta "modale", "finestra di dialogo", "lightbox" o "popup" che non indichi diversamente.

Utilizza <dialog> aperto come finestra modale, in modo dichiarativo con command="show-modal" (consulta Controllo dichiarativo di finestre di dialogo e popup) o dallo script con showModal().

<button type="button" command="show-modal" commandfor="confirm">Delete</button>

<!-- MANDATORY: no role="dialog" or aria-modal; the native modal already provides them -->
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this item?</h2>
  <form method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

Il browser fornisce: pagina inerte, contenimento dello stato attivo, livello superiore, ::backdrop, Esc e chiusura con gesto Indietro e ritorno dello stato attivo all'invoker.

  • NON aggiungere role="dialog", aria-modal, un focus trap, l'attivazione/disattivazione di inert o un gestore Esc.
  • NON aprirlo con show() o con l'attributo open.
  • SÌ: interrompi lo scorrimento della pagina dietro l'annuncio con il CSS riportato di seguito. Per maggiori dettagli, consulta Impedire lo scorrimento della pagina sotto una finestra di dialogo modale.
  • NON bloccare lo scorrimento con overflow: hidden o position: fixed su html o body.
/* MANDATORY (both rules): without them, scrolling over the backdrop or inside the dialog scrolls the page behind it */
dialog:modal {
  overscroll-behavior: contain; /* stops scroll gestures in the dialog chaining to the page */
}
dialog::backdrop {
  overflow: hidden; /* ::backdrop is not a scroll container by default, so it would ignore overscroll-behavior */
  overscroll-behavior: contain;
}

Non per menu o tooltip: vedi la Regola 3.

Regola 2: l'utente continua a lavorare nella pagina mentre il riquadro è aperto

Utilizza questo ruolo per barre di ricerca, strumenti di controllo, tavolozze e riquadri di composizione che sono ancora finestre di dialogo dal punto di vista semantico.

Utilizza <dialog popover> per un riquadro di livello superiore con chiusura del popover (auto chiusura leggera, manual no). Utilizza dialog.show() solo quando il riquadro non deve trovarsi nel livello superiore.

<button type="button" popovertarget="find">Find</button>
<dialog id="find" popover="manual" aria-labelledby="find-title">
  <h2 id="find-title">Find</h2>
  <input type="search" aria-label="Search">
  <button type="button" popovertarget="find" popovertargetaction="hide">Close</button>
</dialog>

La pagina rimane interattiva e non viene resa inerte.

  • SÌ: includi un pulsante di chiusura visibile.
  • NON utilizzare showModal(), che blocca la pagina.

Non per bloccare le conferme: vedi la Regola 1.

Regola 3: l'overlay non è una finestra di dialogo

Utilizzalo per menu, menu a discesa, descrizioni comando, schede popup e notifiche popup.

Utilizza un elemento semantico con l'attributo popover, non <dialog>.

<button type="button" popovertarget="menu">Actions</button>
<ul id="menu" popover>
  <li><a href="/rename">Rename</a></li>
</ul>

Scegli come ignorarlo

L'impostazione predefinita dipende dal modo in cui viene aperta la finestra di dialogo. Se closedby è assente o non valido, showModal() si comporta come closerequest e qualsiasi altro modo di apertura si comporta come none.

  • Solo Esc e gesto Indietro (impostazione predefinita modale): non impostare nulla o closedby="closerequest".
  • Fai clic anche all'esterno per chiudere: utilizza closedby="any".
  • Solo lo sviluppatore lo chiude (ad esempio un modulo con modifiche non salvate): utilizza closedby="none" e aggiungi un percorso di conferma. Consulta la sezione Chiusura di seguito.

Linee guida

Nome accessibile e stato attivo iniziale

  • SÌ: assegna un nome a ogni finestra di dialogo con aria-labelledby che rimanda alla relativa intestazione o aria-label quando non è presente un titolo visibile.
  • SÌ: inserisci autofocus nel controllo principale o nel primo campo.
  • SÌ: inserisci autofocus nell'azione sicura (ad esempio Annulla o Mantieni) in una conferma distruttiva.
  • NON scrivere codice per riportare lo stato attivo alla chiusura. Il browser lo restituisce all'invoker.

Apertura

Chiusura

  • SÌ: chiudi con i pulsanti <form method="dialog"> o command="close".
  • SCEGLI closedby deliberatamente: any (chiusura semplice e Esc), closerequest (solo Esc, l'impostazione predefinita della finestra modale) o none (solo chiusura controllata dallo sviluppatore).
  • SÌ: utilizza command="request-close" o requestClose() per i pulsanti Annulla che devono rispettare la logica delle modifiche non salvate. Attivano prima cancel e ignorano closedby.
  • DA FARE: considera closedby come un potenziamento progressivo. Esc e il gesto Indietro sui dispositivi mobili chiudono già le finestre modali senza questo pulsante.

Hooking into close

  • SÌ: leggi dialog.returnValue in un listener close per sapere quale pulsante è stato utilizzato, quindi reimpostalo con dialog.returnValue = ''. Esc e altre richieste di chiusura non impostano alcun valore e i browser si comportano in modo diverso per quanto riguarda la cancellazione del valore precedente, pertanto senza la reimpostazione potrebbe trapelare un valore non aggiornato. Considera '' come "ignorato senza scelta".
  • SÌ: utilizza cancel per intercettare Esc e request-close, ma non fare affidamento su preventDefault() per pressioni ripetute di Esc senza attivazione utente.
  • SÌ: utilizza closedby="none" con un passaggio di conferma esplicito per i moduli con modifiche non salvate.
  • SÌ: comunica all'utente il motivo per cui la finestra di dialogo è rimasta aperta quando cancel è impedito. Mostra un messaggio visibile e fai riferimento con aria-describedby dal controllo che riceve lo stato attivo (ad esempio Ignora), in modo che uno screen reader lo legga quando lo stato attivo si sposta. Senza questa impostazione, quando lo stato attivo si sposta su un nuovo pulsante, viene annunciato solo il nome del pulsante.
  • NON consentire che la conferma sposti i controlli sotto il puntatore. Riserva lo spazio del messaggio (nascondilo con visibility, non con display o hidden) e aggiungi il pulsante distruttivo in un punto in cui i pulsanti esistenti non si spostano (ad esempio, il primo in una riga allineata a destra), in modo che un secondo clic nello stesso punto non possa annullare le modifiche dell'utente.

Finestre di dialogo non modali

  • SÌ: utilizza <dialog popover> (auto o manual) per un riquadro di livello superiore e dialog.show() quando non vuoi il livello superiore.
  • SÌ: includi un pulsante di chiusura visibile in ogni finestra di dialogo non modale.

Sfondo e stile

Accessibilità

  • NON aggiungere role="dialog", aria-modal, un focus trap manuale o inert per passare a una finestra modale nativa.
  • SÌ: utilizza un <button> nativo con un nome accessibile per i controlli di chiusura.
  • SÌ: utilizza un livello di intestazione coerente per i titoli delle finestre di dialogo.

Strategie di riserva

closedby (dialog-closedby)

Senza closedby, una finestra modale si chiude comunque con Esc e il gesto Indietro; viene persa solo l'azione di clic all'esterno, quindi si tratta di una riduzione controllata. closedby="none" viene ignorato allo stesso modo: Esc attiva cancel e poi chiude la finestra di dialogo. Mantieni la protezione per le modifiche non salvate in un gestore cancel, non solo in closedby="none", in modo che protegga anche i casi in cui manca closedby. Se il target di riferimento richiede la chiusura semplice, aggiungi questo attributo dove manca closedby:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  // Test the pointer position, not event.target: the <dialog> is also the target for its own
  // padding and the gaps between its children, so a target check would close on those clicks
  const outside = (e, dialog) => {
    const r = dialog.getBoundingClientRect();
    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;
  };
  for (const dialog of document.querySelectorAll('dialog[closedby="any"]')) {
    let pressedOutside = false;
    dialog.addEventListener('pointerdown', (e) => { pressedOutside = outside(e, dialog); });
    // Require the press and the release outside, so a text-selection drag that ends on the backdrop does not dismiss
    dialog.addEventListener('pointerup', (e) => { if (pressedOutside && outside(e, dialog)) dialog.close(); });
  }
}

overscroll-behavior

Tratta il contenimento dello scorrimento come un potenziamento progressivo. Non eseguire il rilevamento delle funzionalità: @supports segnala la proprietà come supportata anche se i contenitori non scorrevoli la ignorano. Se non viene applicato, la finestra di dialogo contiene comunque lo scorrimento ogni volta che i suoi contenuti superano il limite e showModal() rende comunque la pagina inerte. Non eseguire il fallback a overflow: hidden o position: fixed su html o body, che rimuove la barra di scorrimento principale e sposta il layout.

Comandi Invoker (command/commandfor)

Se il tuo target Baseline non include i comandi di attivazione, mantieni lo stesso markup e carica il polyfill in modo condizionale (la stessa esperienza, come potenziamento progressivo). Esegui il rilevamento sull'interfaccia e non caricarla mai in modo incondizionato:

// MANDATORY: feature detect on HTMLButtonElement.prototype and load only when native support is missing
if (!('commandForElement' in HTMLButtonElement.prototype)) {
  import('invokers-polyfill'); // npm install invokers-polyfill
}

Solo le regole 2 e 3 utilizzano Popover.

Alternative e supporto del browser per Popover

L'API Popover è per lo più un potenziamento progressivo, ma i suoi comportamenti distintivi (promozione al livello superiore, chiusura con un clic e attivazione di popovertarget) non hanno un equivalente solo CSS. I browser meno recenti richiedono un polyfill o un fallback manuale se preferisci non fornirne uno.

Polyfill: per supportare l'attributo popover nei browser meno recenti, carica @oddbird/popover-polyfill in modo condizionale. OBBLIGATORIO: esegui il rilevamento delle funzionalità controllando la proprietà popover su HTMLElement.prototype e carica il polyfill solo quando manca il supporto nativo; NON caricarlo incondizionatamente.

Con un bundler o una mappa di importazione:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Senza un bundler, importa da una CDN all'interno di un <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Avvertenza sullo stile: il polyfill non può definire la pseudo-classe :popover-open reale, quindi applica una classe .\:popover-open. OBBLIGATORIO: combina i due elementi con :is() o :where(), altrimenti i browser che non dispongono di :popover-open ignorano l'intera regola:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

In alternativa, per un fallback legacy senza un polyfill, utilizza position: fixed e calcola manualmente le coordinate tramite getBoundingClientRect() o affidati al posizionamento predefinito con inset: auto se è accettabile per il caso d'uso.