Dialogkomponente erstellen

Eine grundlegende Übersicht dazu, wie Sie mit dem <dialog>-Element farbadaptive, responsive und barrierefreie Mini- und Mega-Modale erstellen.

In diesem Beitrag möchte ich meine Gedanken dazu teilen, wie man mit dem <dialog>-Element farbadaptive, responsive und barrierefreie Mini- und Mega-Modale erstellt. Demo ausprobieren und Quellcode ansehen

Demonstration der Mega- und Mini-Dialogfelder im hellen und dunklen Design.

Wenn du lieber ein Video ansehen möchtest, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Das Element <dialog> eignet sich hervorragend für kontextbezogene Informationen oder Aktionen auf der Seite. Ü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 der Nutzer nur bestätigen oder abbrechen muss.

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

Browser Support

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Source

Ich habe festgestellt, dass dem Element einige Dinge fehlen. In dieser GUI-Challenge füge ich die erwarteten Elemente für die Entwicklerfreundlichkeit hinzu: zusätzliche Ereignisse, Light Dismiss, benutzerdefinierte Animationen sowie einen Mini- und einen Mega-Typ.

Markieren & Zeichnen

Die Grundlagen eines <dialog>-Elements sind bescheiden. Das Element wird automatisch ausgeblendet und enthält integrierte Stile zum Überlagern Ihrer Inhalte.

<dialog>
  …
</dialog>

Wir können diese Baseline verbessern.

Ein Dialogelement hat traditionell viel mit einem Modal gemeinsam und oft sind die Namen austauschbar. Ich habe mir hier die Freiheit genommen, das Dialogfeld-Element sowohl für kleine Dialogfeld-Pop-ups (Mini) als auch für ganzseitige Dialogfelder (Mega) zu verwenden. Ich habe sie „Mega“ und „Mini“ genannt und beide Dialogfelder leicht an verschiedene Anwendungsfälle angepasst. 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 des Mini- und des Megadialogfelds im hellen und im dunklen Design.

Nicht immer, aber in der Regel werden Dialogelemente verwendet, um Interaktionsinformationen zu erfassen. Formulare in Dialogfeldelementen sind dafür vorgesehen, zusammen verwendet zu werden. Es empfiehlt sich, den Dialoginhalt in ein Formularelement einzuschließen, damit JavaScript auf die vom Nutzer eingegebenen Daten zugreifen kann. Außerdem können Schaltflächen in einem Formular, das method="dialog" verwendet, 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 Megadialogfeld enthält drei Elemente im Formular: <header>, <article>, und <footer>. Sie dienen als semantische Container und als Stilziele für die Darstellung des Dialogfelds. In der Kopfzeile wird der Titel des Modals angezeigt und es gibt eine Schaltfläche zum Schließen. Der Artikel bezieht sich auf Formulareingaben und -informationen. Die Fußzeile enthält eine <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-Ereignishandler onclick. Das Attribut autofocus erhält den Fokus, wenn das Dialogfeld geöffnet wird. Es empfiehlt sich, es auf die Schaltfläche „Abbrechen“ und nicht auf die Schaltfläche „Bestätigen“ zu setzen. So wird sichergestellt, dass die Bestätigung bewusst und nicht versehentlich erfolgt.

Minidialogfeld

Das Mini-Dialogfeld ähnelt dem Mega-Dialogfeld sehr. Es fehlt nur ein <header>-Element. So kann es kleiner und besser in den Textfluss integriert 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 Element mit voller Ansicht, mit dem Daten und Nutzerinteraktionen erfasst werden können. Diese Grundlagen können zu sehr interessanten und leistungsstarken Interaktionen auf Ihrer Website oder in Ihrer App führen.

Bedienungshilfen

Das Dialogfeld hat eine sehr gute integrierte Barrierefreiheit. Anstatt diese Funktionen wie gewohnt hinzuzufügen, sind viele bereits vorhanden.

Fokus wiederherstellen

Wie wir es manuell in Building a sidenav component gemacht haben, ist es wichtig, dass beim Öffnen und Schließen von Elementen der Fokus auf die entsprechenden Schaltflächen zum Öffnen und Schließen gesetzt wird. Wenn die Seitenleiste geöffnet wird, wird der Fokus auf die Schaltfläche zum Schließen gelegt. Wenn die Schließen-Schaltfläche gedrückt wird, wird der Fokus wieder auf die Schaltfläche gelegt, mit der das Fenster geöffnet wurde.

Beim Dialogelement ist dies das integrierte Standardverhalten:

Wenn Sie das Ein- und Ausblenden des Dialogfelds animieren möchten, geht diese Funktion leider verloren. Im JavaScript-Abschnitt werde ich diese Funktion wiederherstellen.

Fokus auf das Fangen

Das Dialogfeld-Element verwaltet inert für Sie im Dokument. Vor inert wurde JavaScript verwendet, um zu beobachten, wann der Fokus ein Element verlässt. In diesem Fall wurde der Fokus abgefangen und zurückgesetzt.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

Nach inert können alle Teile des Dokuments „eingefroren“ werden, sodass sie nicht mehr als Fokusziele dienen oder mit einer Maus interagieren. Anstatt den Fokus zu blockieren, wird er auf den einzigen interaktiven Teil des Dokuments gelenkt.

Element öffnen und automatisch fokussieren

Standardmäßig wird das erste fokussierbare Element im Dialogfeld-Markup fokussiert. Wenn dies nicht das beste Element für den Nutzer ist, verwenden Sie das Attribut autofocus. Wie bereits beschrieben, ist es meiner Meinung nach am besten, diese Meldung auf die Schaltfläche „Abbrechen“ zu legen und nicht auf die Schaltfläche „Bestätigen“. So wird sichergestellt, dass die Bestätigung bewusst und nicht versehentlich erfolgt.

Mit der Esc-Taste schließen

Es ist wichtig, dass Nutzer dieses potenziell störende Element einfach schließen können. Glücklicherweise übernimmt das Dialogelement die Escape-Taste für Sie, sodass Sie sich nicht um die Orchestrierung kümmern müssen.

Stile

Es gibt einen einfachen und einen schwierigen Weg, das Dialogfeld zu gestalten. Der einfache Weg besteht darin, die Display-Eigenschaft des Dialogfelds nicht zu ändern und mit den Einschränkungen zu arbeiten. Ich gehe den harten Weg, um benutzerdefinierte Animationen zum Öffnen und Schließen des Dialogfelds bereitzustellen, die display-Eigenschaft zu übernehmen und vieles mehr.

Styling mit Open Props

Um adaptive Farben und die allgemeine Designkonsistenz zu beschleunigen, habe ich meine CSS-Variablenbibliothek Open Props verwendet. Zusätzlich zu den kostenlosen Variablen importiere ich auch eine normalize-Datei und einige Schaltflächen, die beide von Open Props als optionale Importe bereitgestellt werden. Durch diese Importe kann ich mich auf die Anpassung des Dialogs und der Demo konzentrieren, ohne dass viele Stile erforderlich sind, um das Ergebnis gut aussehen zu lassen.

<dialog>-Element gestalten

Eigentümer der Display-Property

Beim standardmäßigen Ein- und Ausblenden eines Dialogfeldelements wird die Eigenschaft „display“ von block in none geändert. Das bedeutet leider, dass es nicht animiert ein- und ausgeblendet werden kann, sondern nur eingeblendet. Ich möchte die Ein- und Ausblendung animieren. Der erste Schritt besteht darin, die display-Eigenschaft festzulegen:

dialog {
  display: grid;
}

Wenn Sie den Wert der Display-Property ändern und damit festlegen, wie das Element dargestellt wird, wie im obigen CSS-Snippet gezeigt, müssen Sie eine beträchtliche Anzahl von Stilen verwalten, um eine gute Nutzerfreundlichkeit zu gewährleisten. Zuerst ist der Standardstatus eines Dialogfelds „geschlossen“. Sie können diesen Status visuell darstellen und verhindern, dass das Dialogfeld Interaktionen empfängt, indem Sie die folgenden Stile verwenden:

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

Das Dialogfeld ist jetzt unsichtbar und kann nicht verwendet werden, wenn es nicht geöffnet ist. Später füge ich etwas JavaScript hinzu, um das Attribut inert im Dialogfeld zu verwalten. So wird sichergestellt, dass auch Tastatur- und Screenreader-Nutzer den ausgeblendeten Dialog nicht erreichen können.

Dialogfeld ein adaptives Farbdesign zuweisen

Mega-Dialog mit dem hellen und dunklen Design, in dem die Oberflächenfarben zu sehen sind.

Mit color-scheme wird für Ihr Dokument ein vom Browser bereitgestelltes adaptives Farbdesign für helle und dunkle Systemeinstellungen aktiviert. Ich wollte das Dialogfeld jedoch noch weiter anpassen. Open Props bietet einige Oberflächenfarben, die sich automatisch an die hellen und dunklen Systemeinstellungen anpassen, ähnlich wie bei der Verwendung von color-scheme. Sie eignen sich hervorragend zum Erstellen von Ebenen in einem Design. Ich verwende gerne Farben, um die Darstellung von Ebenenoberflächen visuell zu unterstützen. Die Hintergrundfarbe ist var(--surface-1). Verwenden Sie var(--surface-2), um sie über dieser Ebene zu platzieren:

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

Später werden weitere adaptive Farben für untergeordnete Elemente wie Kopf- und Fußzeile hinzugefügt. Ich betrachte sie als zusätzliche Elemente für ein Dialogfeld, die aber sehr wichtig sind, um ein überzeugendes und gut gestaltetes Dialogdesign zu erstellen.

Größenanpassung von responsiven Dialogfeldern

Standardmäßig wird die Größe des Dialogfelds an seinen Inhalt delegiert, was im Allgemeinen gut ist. Mein Ziel ist es, die Breite von max-inline-size auf eine lesbare Größe (--size-content-3 = 60ch) oder 90% der Breite des Darstellungsbereichs zu beschränken. So wird verhindert, dass der Dialog auf einem Mobilgerät über den gesamten Bildschirmrand hinausgeht und auf einem Computerbildschirm so breit ist, dass er schwer zu lesen ist. Dann füge ich ein max-block-size hinzu, damit das Dialogfeld nicht über die Höhe der Seite hinausgeht. 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;
}

Beachten Sie, dass max-block-size zweimal vorkommt. Im ersten Fall wird 80vh verwendet, eine physische Viewport-Einheit. Ich möchte den Dialog für internationale Nutzer im relativen Fluss halten. Daher verwende ich in der zweiten Deklaration die logische, neuere und nur teilweise unterstützte Einheit dvb, wenn sie stabiler wird.

Positionierung von Megadialogfeldern

Um ein Dialogelement zu positionieren, ist es hilfreich, es in seine beiden Teile zu unterteilen: den Vollbildhintergrund und den Dialogcontainer. Der Hintergrund muss alles abdecken und einen Schatteneffekt erzeugen, um zu verdeutlichen, dass sich dieser Dialog im Vordergrund befindet und auf die Inhalte im Hintergrund nicht zugegriffen werden kann. Der Dialogcontainer kann sich über diesem Hintergrund zentrieren und jede Form annehmen, die für seinen Inhalt erforderlich ist.

Mit den folgenden Stilen wird das Dialogfeld-Element am Fenster fixiert, sodass es sich bis in jede Ecke erstreckt. 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);
}
Designs für mobile Mega-Dialogfelder

Bei kleinen Darstellungsbereichen style ich dieses seitenfüllende Megamodal etwas anders. Ich habe den unteren Rand auf 0 festgelegt, wodurch der Dialoginhalt an den unteren Rand des Darstellungsbereichs verschoben wird. Mit einigen Anpassungen des Stils kann ich den Dialog in ein Aktionsblatt umwandeln, das sich näher am 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 der Entwicklertools, die den Randabstand sowohl im geöffneten Mega-Dialog auf dem Computer als auch auf dem Mobilgerät überlagern.

Positionierung von Minidialogfeldern

Bei einem größeren Viewport, z. B. auf einem Desktopcomputer, habe ich die Mini-Dialogfelder über dem Element positioniert, das sie aufgerufen hat. Dazu benötige ich JavaScript. Die von mir verwendete Technik finden Sie hier. Ich denke jedoch, dass sie den Rahmen dieses Artikels sprengt. Ohne das JavaScript wird das Mini-Dialogfeld wie das Mega-Dialogfeld in der Mitte des Bildschirms angezeigt.

Ein echter Blickfang

Fügen Sie dem Dialogfeld noch etwas Flair hinzu, damit es wie eine weiche Oberfläche aussieht, die weit über der Seite liegt. Die Weichheit wird durch das Abrunden der Ecken des Dialogfelds erreicht. Die Tiefe wird mit einer der sorgfältig erstellten Schatten-Properties von Open Props erreicht:

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

Pseudoelement „backdrop“ anpassen

Ich habe den Hintergrund nur leicht bearbeitet und dem Mega-Dialog mit backdrop-filter einen Weichzeichnungseffekt hinzugefügt:

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Source

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

Ich habe auch eine Übergangseigenschaft für backdrop-filter festgelegt, in der Hoffnung, dass Browser in Zukunft Übergänge für das Hintergrundelement zulassen:

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

Screenshot des Megadialogs, der über einem verschwommenen Hintergrund mit bunten Avataren liegt.

Extras für die Gestaltung

Ich nenne diesen Abschnitt „Extras“, weil er mehr mit meiner Demo des Dialogfeldelements zu tun hat als mit dem Dialogfeldelement im Allgemeinen.

Scroll-Einschränkung

Wenn das Dialogfeld angezeigt wird, kann der Nutzer weiterhin die Seite dahinter scrollen. Das möchte ich nicht:

Normalerweise wäre overscroll-behavior meine übliche Lösung, aber gemäß der Spezifikation hat sie keine Auswirkungen auf den Dialog, da es sich nicht um einen Scrollport handelt, d. h. es ist kein Scroller vorhanden, sodass nichts verhindert werden kann. Ich könnte JavaScript verwenden, um auf die neuen Ereignisse aus diesem Leitfaden zu warten, z. B. „closed“ und „opened“, und overflow: hidden im Dokument umschalten. Alternativ könnte ich warten, bis :has() in allen Browsern stabil ist:

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Source

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

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

Das <form>-Layout

Neben der Erfassung der Interaktionsinformationen des Nutzers verwende ich sie hier auch, um die Kopf- und Fußzeile sowie die Artikel-Elemente zu gestalten. Mit diesem Layout möchte ich das untergeordnete Element des Artikels als scrollbaren Bereich darstellen. Ich erreiche dies mit grid-template-rows. Dem Artikel-Element wird 1fr zugewiesen und das Formular selbst hat dieselbe maximale Höhe wie das Dialogfeld-Element. Durch Festlegen dieser festen Höhe und festen Zeilengröße kann das Artikel-Element begrenzt 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 der Entwicklertools, in denen die Informationen zum Rasterlayout über den Zeilen eingeblendet werden.

Dialogfeld <header> gestalten

Dieses Element dient dazu, einen Titel für den Dialoginhalt bereitzustellen und eine leicht zu findende Schaltfläche zum Schließen anzubieten. Außerdem wurde ihm eine Oberflächenfarbe zugewiesen, damit es hinter dem Inhalt des Dialogartikels angezeigt wird. Diese Anforderungen führen zu einem Flexbox-Container, vertikal ausgerichteten Elementen, die an den Rändern ausgerichtet sind, und etwas Padding und Abständen, um dem Titel und den Schließen-Schaltflächen etwas Platz zu geben:

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, in denen Informationen zum Flexbox-Layout über der Dialogfeldüberschrift eingeblendet werden.

Schaltfläche zum Schließen der Kopfzeile gestalten

Da in der Demo die Open Props-Schaltflächen verwendet werden, wird die Schließen-Schaltfläche in ein rundes, zentriertes 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 Informationen zu Größe und Padding für die Schaltfläche zum Schließen des Headers.

Dialogfeld <article> gestalten

Das Artikel-Element hat in diesem Dialogfeld eine besondere Rolle: Es ist ein Bereich, der bei einem hohen oder langen Dialogfeld gescrollt werden soll.

Dazu hat das übergeordnete Formularelement einige Maximalwerte für sich selbst festgelegt, die als Einschränkungen für dieses Artikelelement dienen, wenn es zu hoch wird. Legen Sie overflow-y: auto fest, damit Scrollleisten nur bei Bedarf angezeigt werden. Verwenden Sie overscroll-behavior: contain für das Scrollen innerhalb des Containers. Der Rest wird durch benutzerdefinierte Darstellungsstile festgelegt:

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 Fußzeile enthält Menüs mit Aktionsschaltflächen. Mit Flexbox wird der Inhalt am Ende der Inline-Achse der Fußzeile ausgerichtet. Anschließend wird etwas Abstand eingefügt, um den Schaltflächen etwas 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, in denen Informationen zum Flexbox-Layout des Footer-Elements eingeblendet werden.

Das Element menu enthält die Aktionsschaltflächen für das Dialogfeld. Es wird ein Flexbox-Layout mit Zeilenumbruch und gap verwendet, um Platz zwischen den Schaltflächen zu schaffen. Menüelemente haben einen Innenabstand, z. B. <ul>. Ich entferne diesen Stil auch, 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, in denen Flexbox-Informationen über den Elementen des Fußzeilenmenüs eingeblendet werden.

Animation

Dialogelemente werden oft animiert, wenn sie im Fenster ein- und ausgeblendet werden. Wenn Dialogfelder beim Ein- und Ausblenden animiert werden, können sich Nutzer besser orientieren.

Normalerweise kann das Dialogfeld nur ein- und nicht ausgeblendet werden. Das liegt daran, dass der Browser die display-Property für das Element umschaltet. Bisher wurde die Anzeige im Guide immer auf „Raster“ und nie auf „Keine“ festgelegt. So können Sie Animationen ein- und ausblenden.

Open Props bietet viele Keyframe-Animationen, die sich einfach orchestrieren und lesen lassen. Hier sind die Ziele der Animation und der mehrschichtige Ansatz, den ich gewählt habe:

  1. „Reduzierte Bewegung“ ist der Standardübergang. Dabei wird die Deckkraft einfach ein- und ausgeblendet.
  2. Wenn die Bewegung in Ordnung ist, werden Folien- und Skalierungsanimationen hinzugefügt.
  3. Das responsive mobile Layout für den Megadialog wird so angepasst, dass er herausgeschoben wird.

Sicherer und sinnvoller Standardübergang

Open Props enthält zwar Keyframes für das Ein- und Ausblenden, aber ich bevorzuge diesen mehrschichtigen Ansatz von Übergängen als Standard mit Keyframe-Animationen als potenzielle Upgrades. Wir haben die Sichtbarkeit des Dialogfelds bereits mit der Eigenschaft „opacity“ (Deckkraft) gestaltet und je nach Attribut [open] 1 oder 0 festgelegt. Wenn Sie einen Übergang zwischen 0% und 100 % erstellen möchten, müssen Sie dem Browser mitteilen, wie lange der Übergang dauern soll und welche Art von Easing Sie verwenden möchten:

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

Übergang mit Bewegung

Wenn der Nutzer mit der Bewegung einverstanden ist, sollten sowohl der Mega- als auch der Mini-Dialog beim Einblenden nach oben gleiten und beim Ausblenden verkleinert werden. Das lässt sich mit der Media-Anfrage prefers-reduced-motion und einigen Open Props erreichen:

@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;
  }
}

Ausgangsanimation für Mobilgeräte anpassen

Im vorherigen Abschnitt zum Styling wurde das Mega-Dialogfeld für Mobilgeräte angepasst, sodass es eher wie ein Aktionsblatt aussieht. Es wirkt, als wäre ein kleines Blatt Papier vom unteren Bildschirmrand nach oben geschoben worden und würde immer noch am unteren Rand befestigt sein. Die Scale-out-Schlussanimation passt nicht gut zu diesem neuen Design. Wir können sie aber mit einigen Media-Queries und 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

Es gibt einige Dinge, die Sie mit JavaScript hinzufügen können:

// 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 sind auf den Wunsch nach dem Schließen durch Klicken auf den Dialoghintergrund, Animationen und einigen zusätzlichen Ereignissen zurückzuführen, um die Formulardaten besser abrufen zu können.

Leichtes Schließen hinzufügen

Diese Aufgabe ist einfach und eine gute Ergänzung für ein Dialogelement, das nicht animiert wird. Die Interaktion wird durch das Beobachten von Klicks auf das Dialogfeld-Element erreicht. Dabei wird Event Bubbling verwendet, um zu ermitteln, worauf geklickt wurde. close() wird nur ausgelöst, wenn es sich um das oberste Element handelt:

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

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

Hinweis: dialog.close('dismiss') Das Ereignis wird aufgerufen und ein String wird bereitgestellt. Dieser String kann von anderem JavaScript abgerufen werden, um Informationen dazu zu erhalten, wie das Dialogfeld geschlossen wurde. Außerdem habe ich bei jedem Aufruf der Funktion über verschiedene Schaltflächen auch kurze Strings angegeben, um meiner Anwendung Kontext zur Nutzerinteraktion zu liefern.

Schluss- und geschlossene Ereignisse hinzufügen

Das Dialogfeld-Element wird mit einem Schließereignis ausgeliefert, das sofort ausgegeben wird, wenn die Funktion close() für das Dialogfeld aufgerufen wird. Da wir dieses Element animieren, ist es gut, Ereignisse für vor und nach der Animation zu haben, um die Daten abzurufen oder das Dialogfeld zurückzusetzen. Ich verwende es hier, um das Hinzufügen des Attributs inert im geschlossenen Dialogfeld zu verwalten. In der Demo verwende ich es, um die Avatarliste zu ändern, wenn der Nutzer ein neues Bild gesendet hat.

Erstellen Sie dazu zwei neue Ereignisse mit den Namen closing und closed. Hören Sie dann auf das integrierte Schließereignis im Dialogfeld. Stellen Sie das Dialogfeld auf inert ein und senden Sie das closing-Ereignis. Als Nächstes müssen Sie warten, bis die Animationen und Übergänge im Dialogfeld abgeschlossen sind, und dann das closed-Ereignis senden.

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 im Abschnitt Toast-Komponente erstellen verwendet wird, gibt ein Promise basierend auf dem Abschluss der Animations- und Übergangs-Promises zurück. Aus diesem Grund ist dialogClose eine async-Funktion;. Sie kann dann das zurückgegebene Promise await> und sicher zum geschlossenen Ereignis übergehen.

Eröffnungs- und eröffnete Termine hinzufügen

Diese Ereignisse lassen sich nicht so einfach hinzufügen, da das integrierte Dialogfeld kein „open“-Ereignis wie bei „close“ bietet. Ich verwende einen MutationObserver, um Informationen zu Änderungen an den Attributen des Dialogfelds zu erhalten. In diesem Observer achte ich auf Änderungen am Attribut „open“ und verwalte die benutzerdefinierten Ereignisse entsprechend.

Erstellen Sie wie bei den Ereignissen zum Schließen und den geschlossenen Ereignissen zwei neue Ereignisse mit den Namen opening und opened. Wo wir zuvor auf das Ereignis zum Schließen des Dialogfelds gewartet haben, verwenden wir diesmal 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 Mutation Observer wird aufgerufen, wenn sich die Dialogattribute ändern. Die Liste der Änderungen wird als Array bereitgestellt. Wiederholen Sie die Attributänderungen, bis attributeName geöffnet ist. Prüfen Sie als Nächstes, ob das Element das Attribut hat. So können Sie feststellen, ob das Dialogfeld geöffnet wurde. Wenn es geöffnet wurde, entfernen Sie das Attribut inert und legen Sie den Fokus entweder auf ein Element, das autofocus anfordert, oder auf das erste button-Element im Dialogfeld fest. Ähnlich wie beim Schließen und beim Ereignis „geschlossen“ muss das Ereignis „Öffnen“ sofort gesendet werden. Warten Sie, bis die Animationen abgeschlossen sind, und senden Sie dann das Ereignis „geöffnet“.

Entferntes Ereignis hinzufügen

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

Das lässt sich mit einem weiteren Mutation Observer erreichen. Dieses Mal beobachten wir nicht die Attribute eines Dialogfeldelements, sondern die untergeordneten Elemente des Body-Elements und achten darauf, ob Dialogfeldelemente 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 Mutation Observer-Callback wird aufgerufen, wenn dem Body des Dokuments untergeordnete Elemente hinzugefügt oder daraus entfernt werden. Die beobachteten Mutationen beziehen sich auf removedNodes mit dem nodeName eines Dialogs. Wenn ein Dialogfeld entfernt wurde, werden die Klick- und Schließen-Ereignisse entfernt, um Speicherplatz freizugeben. Das benutzerdefinierte Ereignis „removed“ wird gesendet.

Attribut „loading“ entfernen

Damit die Dialogfeldanimation beim Hinzufügen des Dialogfelds zur Seite oder beim Laden der Seite nicht mit der Exit-Animation beginnt, wurde dem Dialogfeld ein „loading“-Attribut hinzugefügt. Das folgende Skript wartet, bis die Dialogfeldanimationen abgeschlossen sind, und entfernt dann das Attribut. Jetzt kann das Dialogfeld ein- und ausgeblendet werden und wir haben eine ansonsten störende Animation ausgeblendet.

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

Weitere Informationen zum Problem, Keyframe-Animationen beim Laden der Seite zu verhindern

Insgesamt

Hier ist dialog.js in seiner Gesamtheit, 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')
}

dialog.js-Modul verwenden

Die exportierte Funktion des Moduls erwartet, dass sie aufgerufen wird und ein Dialogfeld-Element übergeben wird, dem diese 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)

Die beiden Dialogfelder wurden mit der Funktion zum Schließen durch Tippen auf den Hintergrund, Korrekturen für das Laden von Animationen und weiteren Ereignissen aktualisiert.

Auf die neuen benutzerdefinierten Ereignisse warten

Jedes aktualisierte Dialogelement kann jetzt auf fünf neue Ereignisse warten, z. B.:

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

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

MegaDialog.addEventListener('removed', dialogRemoved)

Hier sind zwei Beispiele für den Umgang mit diesen Ereignissen:

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 dieses „closed“-Ereignis und die Formulardaten, um der Liste ein neues Avatar-Element hinzuzufügen. Das Timing ist gut, da das Dialogfeld die Exit-Animation abgeschlossen hat und dann einige Skripts den neuen Avatar animieren. Dank der neuen Ereignisse lässt sich das Nutzererlebnis besser steuern.

dialog.returnValue: Enthält den Close-String, der übergeben wird, wenn das Ereignis close() des Dialogfelds aufgerufen wird. Beim dialogClosed-Ereignis ist es wichtig zu wissen, ob der Dialog geschlossen, abgebrochen oder bestätigt wurde. Wenn die Bestätigung erfolgt ist, ruft das Script die Formularwerte ab und setzt das Formular zurück. Das Zurücksetzen ist nützlich, damit das Dialogfeld, wenn es wieder angezeigt wird, leer ist und für eine neue Einreichung bereit ist.

Fazit

Jetzt wissen Sie, wie ich es gemacht habe. Wie würden Sie vorgehen? 🙂

Wir möchten unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, die das Web bietet.

Erstelle eine Demo, schick mir einen Tweet mit den Links und ich füge sie unten im Bereich „Community-Remixe“ hinzu.

Community-Remixe

Ressourcen