Dialogkomponente erstellen

Ein grundlegender Überblick darüber, wie du mit dem <dialog>-Element farbadaptive, responsive und barrierefreie Mini- und Mega-Modals erstellen kannst.

In diesem Beitrag möchte ich meine Gedanken zum Erstellen von farbadaptiven, responsiven und barrierefreien Mini- und Mega-Modals mit dem Element <dialog> teilen. Demo ansehen und Quelle ansehen

Demonstration der Mega- und Mini-Dialoge in ihren hellen und dunklen Designs.

Falls Sie Videos bevorzugen, finden Sie hier eine YouTube-Version dieses Beitrags:

Überblick

Das Element <dialog> eignet sich hervorragend für In-Page-Kontextinformationen oder Aktionen. Überlegen Sie, wann die Nutzerfreundlichkeit von einer Aktion auf derselben Seite anstelle einer mehrseitigen Aktion profitieren kann, z. B. weil das Formular klein ist oder die einzige Aktion des Nutzers das Bestätigen oder Abbrechen ist.

Das <dialog>-Element ist seit Kurzem browserübergreifend stabil:

Unterstützte Browser

  • 37
  • 79
  • 98
  • 15,4

Quelle

Bei dem Element fehlen einige Dinge. Daher füge ich in dieser GUI-Challenge die von mir erwarteten Elemente für die Entwicklererfahrung hinzu: zusätzliche Ereignisse, leichtes Schließen, benutzerdefinierte Animationen sowie einen Mini- und Megatyp.

Markup

Die wesentlichen Elemente eines <dialog>-Elements sind bescheiden. Das Element wird automatisch ausgeblendet und es sind Stile integriert, die Ihre Inhalte überlagern.

<dialog>
  …
</dialog>

Wir können diese Baseline verbessern.

Normalerweise hat ein Dialogelement ähnliche Elemente wie ein modales Element und die Namen sind oft austauschbar. Ich habe das Dialogelement hier sowohl für kleine als auch für ganze Seitendialogfelder (Mega) verwendet. Ich nannte sie „Mega“ und „Mini“, wobei beide Dialoge leicht an verschiedene Anwendungsfälle angepasst wurden. Ich habe ein modal-mode-Attribut hinzugefügt, mit dem Sie den Typ angeben können:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

Screenshot der Mini- und der Mega-Dialoge in hellem und dunklem Design.

Nicht immer, aber in der Regel werden Dialogelemente verwendet, um einige Interaktionsinformationen zu erfassen. Formulare innerhalb von Dialogelementen sind aufeinander abgestimmt. Es empfiehlt sich, den Inhalt des Dialogfelds von einem Formularelement umschließen zu lassen, damit JavaScript auf die vom Nutzer eingegebenen Daten zugreifen kann. Außerdem können Schaltflächen in einem Formular, die method="dialog" verwenden, ein Dialogfeld ohne JavaScript schließen und Daten übergeben.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Mega-Dialogfeld

Ein Mega-Dialogfeld hat drei Elemente innerhalb des Formulars: <header>, <article> und <footer>. Diese dienen als semantische Container sowie Stilziele für die Darstellung des Dialogfelds. Der Header betitelt das modale Fenster und bietet eine Schaltfläche zum Schließen. Der Artikel bezieht sich auf Formulareingaben und -informationen. Die Fußzeile enthält <menu> mit Aktionsschaltflächen.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

Die erste Menüschaltfläche hat autofocus und einen Inline-Event-Handler onclick. Das Attribut autofocus ist fokussiert, wenn das Dialogfeld geöffnet wird. Ich denke, es empfiehlt sich, dies auf der Schaltfläche „Abbrechen“ und nicht auf der Schaltfläche „Bestätigen“ zu platzieren. Dadurch wird sichergestellt, dass die Bestätigung absichtlich erfolgt und nicht versehentlich erfolgt.

Mini-Dialogfeld

Das Mini-Dialogfeld ist dem Mega-Dialogfeld sehr ähnlich, es fehlt nur ein <header>-Element. Dadurch kann es kleiner und inline angezeigt werden.

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

Das Dialogelement bietet eine solide Grundlage für ein vollständiges Darstellungsbereichselement, mit dem Daten und Nutzerinteraktionen erfasst werden können. Diese Grundlagen können zu sehr interessanten und wirkungsvollen Interaktionen auf Ihrer Website oder in Ihrer App führen.

Bedienungshilfen

Das Dialogelement verfügt über eine sehr gute integrierte Zugänglichkeit. Anstatt diese Funktionen wie gewohnt hinzuzufügen, sind viele bereits verfügbar.

Fokus wird wiederhergestellt

Wie im Abschnitt Sidenav-Komponente erstellen bereits beschrieben, ist es wichtig, dass beim Öffnen und Schließen die relevanten Schaltflächen zum Öffnen und Schließen in den Fokus rücken. Wenn diese seitliche Navigationsleiste geöffnet wird, liegt der Fokus auf der Schließen-Schaltfläche. Wenn die Schaltfläche „Schließen“ gedrückt wird, wird der Fokus wieder auf der Schaltfläche gesetzt, mit der sie geöffnet wurde.

Mit dem Dialogelement ist dies in das Standardverhalten integriert:

Wenn Sie das Dialogfenster animieren möchten, geht diese Funktion leider verloren. Im Abschnitt JavaScript stelle ich diese Funktion wieder her.

Überfangfokus

Das Dialogelement verwaltet inert für Sie im Dokument. Vor inert wurde JavaScript verwendet, um zu prüfen, ob ein Element fokussiert wurde. Dieses Element fängt es ab und platziert es wieder.

Unterstützte Browser

  • 102
  • 102
  • 112
  • 15.5

Quelle

Nach dem inert können Teile des Dokuments „eingefroren“ sein, solange sie keine Fokusziele mehr sind oder mit einer Maus interaktiv sind. Anstatt den Fokus zu überdecken, wird der Fokus auf den einzigen interaktiven Teil des Dokuments gelenkt.

Element öffnen und automatisch fokussieren

Standardmäßig weist das Dialogelement den Fokus dem ersten fokussierbaren Element im Dialog-Markup zu. Wenn dies nicht das beste Element für den Nutzer ist, verwende das Attribut autofocus. Wie bereits erwähnt, empfiehlt es sich, dies auf die Schaltfläche „Abbrechen“ und nicht auf die Schaltfläche „Bestätigen“ zu setzen. So wird sichergestellt, dass die Bestätigung beabsichtigt und nicht versehentlich erfolgt.

Wird mit der Escape-Taste geschlossen

Es ist wichtig, dieses potenziell störende Element einfach zu schließen. Glücklicherweise verarbeitet das Dialogelement die Escape-Taste für Sie, sodass Sie die Orchestrierungszeit entlasten.

Stile

Es gibt eine einfache Möglichkeit, das Dialogelement zu gestalten, und einen harten Pfad. Der einfache Pfad wird dadurch erreicht, dass die Anzeigeeigenschaft des Dialogfelds nicht geändert und die Einschränkungen eingehalten werden. Ich benutzerdefinierte Animationen zum Öffnen und Schließen des Dialogfelds und übernehme dabei unter anderem das Attribut display.

Stile mit offenen Requisiten

Um die Anpassung von Farben und die Einheitlichkeit des Designs insgesamt zu beschleunigen, habe ich meine CSS-Variablenbibliothek Open Props implementiert. Zusätzlich zu den kostenlos bereitgestellten Variablen importiere ich auch eine Normalize-Datei und einige Schaltflächen, die beide in Open Props als optionale Importe zur Verfügung stehen. Diese Importe helfen mir dabei, das Dialogfeld und die Demo anzupassen, ohne viele Stile zu benötigen, um es zu unterstützen und gut aussehen zu lassen.

<dialog>-Element gestalten

Inhaber der Property „display“

Mit dem Standardverhalten zum Ein- und Ausblenden eines Dialogelements wird die Anzeigeeigenschaft von block zu none umgeschaltet. Das bedeutet leider, dass sie nicht nur innen animiert werden können. Ich möchte eine Animation sowohl ein- als auch aussteigen und der erste Schritt besteht darin, meine eigene display-Eigenschaft festzulegen:

dialog {
  display: grid;
}

Wie im obigen CSS-Snippet gezeigt, müssen Sie eine erhebliche Anzahl von Stilen verwalten, um die Nutzerfreundlichkeit zu verbessern, indem Sie den Wert der display-Eigenschaft ändern und dementsprechend Inhaber des Werts sind. Zuerst wird der Standardstatus eines Dialogfelds geschlossen. Mit den folgenden Stilen können Sie diesen Status visuell darstellen und verhindern, dass das Dialogfeld Interaktionen empfängt:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

Jetzt ist das Dialogfeld unsichtbar und es kann nicht mit ihm interagiert werden, solange es nicht geöffnet ist. Später werde ich JavaScript-Code einfügen, um das Attribut inert im Dialogfeld zu verwalten, damit auch Nutzer von Tastaturen und Screenreadern das ausgeblendete Dialogfeld nicht erreichen können.

Dem Dialogfeld ein adaptives Farbdesign geben

Mega-Dialogfeld mit dem hellen und dunklen Design, das die Farben der Oberfläche zeigt.

Mit color-scheme wird Ihr Dokument für ein vom Browser bereitgestelltes adaptives Farbdesign an die hellen und dunklen Systemeinstellungen aktiviert. Ich wollte das Dialogfeldelement aber noch weiter anpassen. Open Props bietet einige Oberflächenfarben, die sich automatisch an helle und dunkle Systemeinstellungen anpassen, ähnlich wie bei Verwendung von color-scheme. Sie eignen sich hervorragend zum Erstellen von Ebenen in einem Design und ich liebe es, Farben zur visuellen Unterstützung dieser Darstellung von Ebenenoberflächen zu verwenden. Die Hintergrundfarbe ist var(--surface-1). Um auf diese Ebene zu setzen, verwenden Sie var(--surface-2):

dialog {
  …
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

Für untergeordnete Elemente wie die Kopf- und Fußzeile werden später weitere adaptive Farben hinzugefügt. Ich betrachte sie als Zusatzelemente als Dialogelement, aber sehr wichtig für ein überzeugendes und gut durchdachtes Dialogdesign.

Größe des responsiven Dialogfelds anpassen

Das Dialogfeld delegiert seine Größe standardmäßig an seinen Inhalt, was im Allgemeinen hervorragend ist. Mein Ziel ist es, max-inline-size auf eine lesbare Größe (--size-content-3 = 60ch) bzw. 90% der Breite des Darstellungsbereichs zu beschränken. Dadurch wird sichergestellt, dass das Dialogfeld auf einem Mobilgerät nicht randlos erscheint und auf einem Desktop-Bildschirm nicht so breit ist, dass es schwer lesbar ist. Dann füge ich einen max-block-size hinzu, damit das Dialogfeld die Höhe der Seite nicht überschreitet. Das bedeutet auch, dass wir angeben müssen, wo sich der scrollbare Bereich des Dialogfelds befindet, falls es sich um ein hohes Dialogfeldelement handelt.

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

Siehst du, dass ich max-block-size zweimal habe? Im ersten Beispiel wird 80vh verwendet, eine physische Darstellungseinheit. Ich möchte, dass der Dialog innerhalb eines relativen Ablaufs für internationale Nutzer bleibt. Deshalb verwende ich die logische, neuere und nur teilweise unterstützte dvb-Einheit in der zweiten Deklaration, wenn sie stabiler wird.

Positionierung des Mega-Dialogfelds

Für die Positionierung eines Dialogelements empfiehlt es sich, seine beiden Teile aufzuschlüsseln: den Vollbildhintergrund und den Dialogcontainer. Der Hintergrund muss alles abdecken und einen Schattierungseffekt aufweisen, der zeigt, dass sich dieses Dialogfeld im Vordergrund befindet und die dahinter liegenden Inhalte nicht zugänglich sind. Der Dialogcontainer kann sich über diesen Hintergrund zentrieren und die Form annehmen, die sein Inhalt erfordert.

Mit den folgenden Stilen wird das Dialogelement am Fenster fixiert, sodass es bis in jede Ecke gestreckt wird. Außerdem wird margin: auto verwendet, um den Inhalt zu zentrieren:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Stile des Mega-Dialogfelds für Mobilgeräte

Bei kleinen Darstellungsbereichen gestalte ich dieses Vollbild-Modaldialogfeld etwas anders. Ich lege den unteren Rand auf 0 fest. Dadurch wird der Inhalt des Dialogfelds unten im Darstellungsbereich angezeigt. Mit ein paar Stilanpassungen kann ich das Dialogfeld in ein Aktionsblatt verwandeln, das sich näher an den Daumen des Nutzers befindet:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Screenshot von Entwicklertools mit überlagerndem Randabstand sowohl im Mega-Dialogfeld für Desktop als auch auf dem Mobilgerät, während es geöffnet ist.

Mini-Dialogpositionierung

In einem größeren Darstellungsbereich, z. B. auf einem Desktop-Computer, habe ich die Mini-Dialogfelder über dem Element platziert, mit dem sie aufgerufen wurden. Dazu benötige ich JavaScript. Die von mir verwendete Methode finden Sie hier, aber ich glaube, dass sie den Rahmen dieses Artikels sprengen. Ohne JavaScript erscheint das Mini-Dialogfeld genau wie das Mega-Dialogfeld in der Mitte des Bildschirms.

Mach es knackig

Verleihen Sie dem Dialog auch etwas Flair, sodass er wie eine weiche Oberfläche aussieht, die weit über der Seite ragt. Diese wird durch Abrunden der Ecken des Dialogfelds erreicht. Die Tiefe wird mit einem der sorgfältig angefertigten Schattenrequisiten von Open Props erreicht:

dialog {
  …
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

Hintergrund-Pseudoelement anpassen

Ich habe sehr leicht mit dem Hintergrund gearbeitet und dem Mega-Dialogfeld mit backdrop-filter einen Weichzeichnereffekt hinzugefügt:

Unterstützte Browser

  • 76
  • 79
  • 103
  • 9

Quelle

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

Außerdem habe ich mich für backdrop-filter entschieden, in der Hoffnung, dass Browser die Umstellung des Hintergrundelements in Zukunft ermöglichen:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Screenshot des Mega-Dialogfelds mit weichgezeichnetem Hintergrund von bunten Avataren.

Stile für Extras

Ich nenne diesen Abschnitt "Extras", da er mehr mit der Demo des Dialogelements zu tun hat als mit dem Dialogelement im Allgemeinen.

Begrenzungen scrollen

Wenn das Dialogfeld angezeigt wird, kann der Nutzer weiterhin auf der dahinter liegenden Seite scrollen, was ich nicht möchte:

Normalerweise wäre overscroll-behavior meine übliche Lösung, aber gemäß der Spezifikation hat dies keine Auswirkungen auf das Dialogfeld, da es sich nicht um einen Scrollport handelt, also nicht um einen Scroller, sodass nichts verhindert werden kann. Ich könnte mithilfe von JavaScript nach neuen Ereignissen aus dieser Anleitung suchen, z. B. „closed“ und „opened“, und overflow: hidden im Dokument aktivieren oder warten, bis :has() in allen Browsern stabil ist:

Unterstützte Browser

  • 105
  • 105
  • 121
  • 15,4

Quelle

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

Wenn jetzt ein Mega-Dialogfeld geöffnet ist, enthält das HTML-Dokument overflow: hidden.

Das <form>-Layout

Das ist nicht nur ein sehr wichtiges Element, um Interaktionsinformationen vom Nutzer zu erfassen, sondern auch das Layout von Kopf-, Fußzeilen- und Artikelelementen. Mit diesem Layout möchte ich das untergeordnete Artikelelement als scrollbaren Bereich artikulieren. Das erreiche ich mit grid-template-rows. Das Artikelelement erhält den Wert 1fr und das Formular selbst hat dieselbe maximale Höhe wie das dialogelement. Wenn Sie diese feste Höhe und Zeilengröße festlegen, kann das Artikelelement fixiert werden und scrollen, wenn es überläuft:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Screenshot von Entwicklertools, die die Rasterlayoutinformationen über die Zeilen einblenden.

Stil des Dialogfelds <header> festlegen

Die Rolle dieses Elements besteht darin, einen Titel für den Dialoginhalt und eine einfach zu findende Schließen-Schaltfläche anzubieten. Sie erhält auch eine Oberflächenfarbe, damit sie sich hinter dem Inhalt des Dialogartikels befindet. Diese Anforderungen führen zu einem Flexbox-Container, vertikal ausgerichteten Elementen mit Abstand zu ihren Rändern sowie einigen Innenabständen und Lücken, damit die Titel- und Schließschaltflächen etwas Platz haben:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

Screenshot der Chrome-Entwicklertools mit Flexbox-Layoutinformationen in der Kopfzeile des Dialogfelds.

Stil für die Schließen-Schaltfläche in der Kopfzeile festlegen

Da in der Demo die Schaltflächen „Props öffnen“ verwendet werden, wird die Schaltfläche „Schließen“ in eine runde Schaltfläche mit Symbol umgewandelt:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

Screenshot der Chrome-Entwicklertools mit eingeblendeten Größen- und Padding-Informationen für die Schließen-Schaltfläche der Kopfzeile.

Stil des Dialogfelds <article> festlegen

Das Artikelelement hat in diesem Dialogfeld eine besondere Rolle: Es ist ein Bereich, in dem bei einem langen oder langen Dialogfeld gescrollt werden kann.

Dazu hat das übergeordnete Formularelement einige Höchstwerte festgelegt, die Einschränkungen für das Artikelelement vorgeben, die erreicht werden kann, wenn es zu hoch wird. Legen Sie overflow-y: auto so fest, dass Bildlaufleisten nur bei Bedarf angezeigt werden und Scrollen darin mit overscroll-behavior: contain enthalten. Der Rest sind benutzerdefinierte Präsentationsstile:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

Die Rolle der Fußzeile besteht darin, Menüs mit Aktionsschaltflächen zu enthalten. Flexbox wird verwendet, um den Inhalt am Ende der Inline-Achse der Fußzeile auszurichten. Anschließend folgt ein gewisser Abstand, um den Schaltflächen Platz zu geben.

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

Screenshot der Chrome-Entwicklertools mit Flexbox-Layoutinformationen im Fußzeilenelement.

Das Element menu enthält die Aktionsschaltflächen für das Dialogfeld. Dabei wird ein umschließendes Flexbox-Layout mit gap verwendet, um Platz zwischen den Schaltflächen zu schaffen. Für Menüelemente gibt es einen Innenrand wie z. B. einen <ul>. Ich entferne auch diesen Stil, da ich ihn nicht benötige.

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

Screenshot der Chrome-Entwicklertools mit Flexbox-Informationen über den Menüelementen in der Fußzeile.

Animation

Dialogfeldelemente sind häufig animiert, da sie das Fenster betreten und verlassen. Wenn du Dialogen eine unterstützende Bewegung für diesen Ein- und Ausgang gibst, können sich die Nutzenden im Ablauf orientieren.

Normalerweise kann das Dialogelement nur inner- und nicht außen animiert werden. Das liegt daran, dass der Browser das Attribut display für das Element umschaltet. Bisher wurde die Anzeige auf „Raster“ gesetzt und nie auf „Keine“ festgelegt. Dadurch können Sie ein- und ausblenden.

Open Props enthält viele Keyframe-Animationen zur Verwendung, was die Orchestrierung einfach und gut lesbar macht. Hier sind meine Animationsziele und der mehrschichtige Ansatz:

  1. Reduzierte Bewegung ist der Standardübergang, ein einfaches Ein- und Ausblenden der Deckkraft.
  2. Wenn die Bewegung in Ordnung ist, werden Folien- und Skalierungsanimationen hinzugefügt.
  3. Das responsive mobile Layout für das Mega-Dialogfeld wird so angepasst, dass es herauszoomt.

Sicherer und sinnvoller Standardwechsel

Während Open Props Keyframes zum Ein- und Ausblenden bietet, bevorzuge ich diesen mehrschichtigen Ansatz für Übergänge als Standard und Keyframe-Animationen als potenzielle Upgrades. Wir haben die Sichtbarkeit des Dialogfelds bereits mit Deckkraft gestaltet und 1 oder 0 je nach [open]-Attribut orchestriert. Für einen Wechsel zwischen 0% und 100 % teilen Sie dem Browser mit, wie lange und welche Art von Easing Sie verwenden möchten:

dialog {
  transition: opacity .5s var(--ease-3);
}

Dem Übergang Bewegung hinzufügen

Wenn der Nutzer mit Bewegungen einverstanden ist, sollten sich sowohl das Mega- als auch das Mini-Dialogfeld als Einstieg nach oben bewegen und als Ausgang hochskalieren. Dazu können Sie die Medienabfrage prefers-reduced-motion und einige Open Props verwenden:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

Exit-Animation für Mobilgeräte anpassen

Im Abschnitt „Stile“ wurde der Stil des Mega-Dialogs bereits früher für Mobilgeräte so angepasst, dass es mehr wie ein Aktionsblatt aussieht, als wäre ein kleines Blatt Papier vom unteren Displayrand nach oben gerissen worden und es ist immer noch unten angebracht. Die Exit-Animation mit horizontaler Skalierung passt nicht gut zu diesem neuen Design. Wir können sie mit ein paar Medienabfragen und einigen Open Props anpassen:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

Mit JavaScript müssen Sie einige Dinge hinzufügen:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

Diese Ergänzungen ergeben sich aus dem Wunsch nach Lichtabschaltung (durch Klicken auf den Dialoghintergrund), Animationen und einigen zusätzlichen Ereignissen für ein besseres Timing beim Abrufen der Formulardaten.

Licht beim Hinzufügen ausschalten

Diese Aufgabe ist unkompliziert und eine tolle Ergänzung für ein nicht animiertes Dialogelement. Die Interaktion erfolgt, indem Klicks auf das Dialogelement beobachtet und mithilfe von Ereignis-Bubbling ermittelt wird, was angeklickt wurde. close() wird nur verwendet, wenn es das oberste Element ist:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Beachten Sie dialog.close('dismiss'). Das Ereignis wird aufgerufen und ein String wird bereitgestellt. Dieser String kann von anderem JavaScript abgerufen werden, um Informationen darüber zu erhalten, wie das Dialogfeld geschlossen wurde. Sie werden feststellen, dass ich jedes Mal, wenn ich die Funktion über verschiedene Schaltflächen aufrufe, Schließstrings bereitgestellt habe, um meiner Anwendung Kontext zur Nutzerinteraktion zu liefern.

Schließende und geschlossene Ereignisse hinzufügen

Das Dialogelement enthält ein Schließereignis: Es wird sofort ausgelöst, wenn die Dialogfunktion close() aufgerufen wird. Da dieses Element animiert wird, ist es schön, Ereignisse für vor und nach der Animation zu haben, um eine Änderung zum Abrufen der Daten oder zum Zurücksetzen des Dialogformulars zu ermöglichen. Ich verwende es hier, um das Hinzufügen des Attributs inert zum geschlossenen Dialogfeld zu verwalten, und in der Demo, um die Avatarliste zu ändern, wenn der Nutzer ein neues Bild eingereicht hat.

Erstellen Sie dazu zwei neue Ereignisse mit den Namen closing und closed. Warten Sie dann auf das integrierte Ereignis „Schließen“ im Dialogfeld. Legen Sie dann das Dialogfeld auf inert fest und senden Sie das Ereignis closing. Als Nächstes wird gewartet, bis die Animationen und Übergänge im Dialogfeld vollständig ausgeführt wurden. Dann wird das Ereignis closed ausgelöst.

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  …
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

Die Funktion animationsComplete, die auch in der Toast-Komponente erstellen verwendet wird, gibt ein Promise basierend auf dem Abschluss der Animation und der Übergangsversprechen zurück. Aus diesem Grund ist dialogClose eine asynchrone Funktion. Sie kann dann das zurückgegebene Promise await und dann souverän zum geschlossenen Ereignis übergehen.

Öffnende und geöffnete Ereignisse hinzufügen

Diese Ereignisse lassen sich nicht so einfach hinzufügen, da das integrierte dialogelement kein Open-Ereignis wie beim Schließen bereitstellt. Ich verwende einen MutationObserver, um Informationen zu den Änderungen der Attribute des Dialogfelds bereitzustellen. In diesem Beobachter achte ich auf Änderungen am Attribut „open“ und verwalte die benutzerdefinierten Ereignisse entsprechend.

Erstellen Sie ähnlich wie beim Starten von schließenden und geschlossenen Ereignissen zwei neue Ereignisse mit den Namen opening und opened. Bisher haben wir auf das Schließen des Dialogfelds gewartet. Jetzt verwenden wir einen erstellten Mutationsbeobachter, um die Attribute des Dialogfelds zu beobachten.

…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  …
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

Die Callback-Funktion des Mutationsobservers wird aufgerufen, wenn die Dialogattribute geändert werden, und stellt die Liste der Änderungen als Array bereit. Iterieren Sie über die Attributänderungen und achten Sie darauf, dass attributeName offen ist. Prüfen Sie als Nächstes, ob das Element das Attribut hat. Dies gibt an, ob das Dialogfeld geöffnet wurde. Entfernen Sie das Attribut inert, falls es geöffnet wurde, und legen Sie den Fokus entweder auf ein Element, das autofocus anfordert, oder auf das erste button-Element im Dialogfeld. Als Letztes wird, ähnlich wie beim Schließen- und Schließen-Ereignis, das Eröffnungsereignis sofort ausgelöst. Warten Sie, bis die Animationen beendet sind, und lösen Sie dann das geöffnete Ereignis aus.

Entfernte Termine hinzufügen

In Single-Page-Anwendungen werden Dialogfelder häufig basierend auf Routen oder anderen Anwendungsanforderungen und -status hinzugefügt und entfernt. Es kann hilfreich sein, Ereignisse oder Daten zu bereinigen, wenn ein Dialogfeld entfernt wird.

Sie können dies mit einem anderen Mutationsbeobachter erreichen. Dieses Mal beobachten wir nicht die Attribute eines Dialogelements, sondern beobachten die untergeordneten Elemente des Textelements und achten darauf, dass Dialogelemente entfernt werden.

…
const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  …
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

Der Mutationsobserver-Callback wird aufgerufen, wenn untergeordnete Elemente dem Textkörper des Dokuments hinzugefügt oder daraus entfernt werden. Die beobachteten Mutationen beziehen sich auf removedNodes mit dem nodeName eines Dialogfelds. Wenn ein Dialogfeld entfernt wurde, werden die Klick- und Schließereignisse entfernt, um Arbeitsspeicher freizugeben, und das benutzerdefinierte Ereignis wird ausgelöst.

Ladeattribut wird entfernt

Damit die Dialogfeldanimation beim Hinzufügen zur Seite oder beim Seitenaufbau nicht mehr wiedergegeben wird, wurde dem Dialogfeld ein Ladeattribut hinzugefügt. Das folgende Skript wartet, bis die Dialoganimationen vollständig ausgeführt wurden, und entfernt dann das Attribut. Jetzt kann das Dialogfeld animiert werden und wir haben eine ansonsten ablenkende Animation praktisch ausgeblendet.

export default async function (dialog) {
  …
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Weitere Informationen zum Verhindern von Keyframe-Animationen beim Seitenaufbau

Alles zusammen

Hier sehen Sie die vollständige Version von dialog.js, nachdem wir jeden Abschnitt einzeln erläutert haben:

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Modul dialog.js verwenden

Die aus dem Modul exportierte Funktion erwartet, dass sie aufgerufen und ein Dialogelement übergeben wird, mit dem die folgenden neuen Ereignisse und Funktionen hinzugefügt werden sollen:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

So werden die beiden Dialogfelder aktualisiert und mit einem einfachen Schließen versehen, Fehlerkorrekturen beim Laden von Animationen und weiteren Ereignissen, mit denen Sie arbeiten können.

Neue benutzerdefinierte Ereignisse überwachen

Jedes aktualisierte Dialogelement kann jetzt auf fünf neue Ereignisse warten:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

Hier zwei Beispiele für die Verarbeitung dieser Ereignisse:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

In der Demo, die ich mit dem Dialogelement erstellt habe, verwende ich das Closed-Ereignis und die Formulardaten, um der Liste ein neues Avatar-Element hinzuzufügen. Das Timing ist gut, da die Exit-Animation des Dialogfelds abgeschlossen ist und dann einige Skripts im neuen Avatar animiert werden. Dank der neuen Ereignisse kann die Orchestrierung der User Experience reibungsloser sein.

Beachten Sie dialog.returnValue: Dies enthält den String für das Schließen, der beim Aufruf des Dialogfelds close() übergeben wird. Für das dialogClosed-Ereignis ist es wichtig zu wissen, ob das Dialogfeld geschlossen, abgebrochen oder bestätigt wurde. Ist dies bestätigt, erfasst das Skript die Formularwerte und setzt das Formular zurück. Das Zurücksetzen ist nützlich, damit das Dialogfeld, das wieder angezeigt wird, leer und bereit für eine neue Einreichung ist.

Fazit

Jetzt, wo du weißt, wie ich es gemacht habe, wie würdest du... ‽ 🙂

Lassen Sie uns unsere Herangehensweisen diversifizieren und alle Möglichkeiten kennenlernen, wie wir das Web entwickeln können.

Erstelle eine Demo, Tweets an mich und füge sie unten im Abschnitt zu Community-Remixen hinzu.

Community-Remixe

Weitere Informationen