Dialogkomponente erstellen

Eine grundlegende Übersicht dazu, wie Sie mit dem Element <dialog> farbadaptiv, responsiv und barrierefrei nutzbare Mini- und Mega-Modalfenster erstellen.

In diesem Beitrag möchte ich meine Gedanken zum Erstellen von farbadaptiven, responsiven und barrierefreien Mini- und Mega-Modals mit dem Element <dialog> teilen. Probieren Sie die Demo aus und sehen Sie sich die Quelle an.

Demonstration der Mega- und Minidialoge im hellen und dunklen Design.

Wenn du lieber ein Video ansiehst, 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 statt einer mehrseitigen Aktion profitieren kann: Vielleicht, weil das Formular klein ist oder die einzige Aktion, die vom Nutzer erforderlich ist, die Bestätigung oder das Abbrechen ist.

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

Unterstützte Browser

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

Quelle

Ich habe festgestellt, dass dem Element einige Dinge fehlen. Deshalb füge ich in dieser GUI-Herausforderung die Elemente hinzu, die ich von der Entwicklererfahrung erwarte: zusätzliche Ereignisse, eine einfache Schließung, benutzerdefinierte Animationen und Mini- und Mega-Typen.

Markieren & Zeichnen

Die wesentlichen Bestandteile eines <dialog>-Elements sind überschaubar. Das Element wird automatisch ausgeblendet und es sind Stile integriert, mit denen Sie Ihre Inhalte überlagern können.

<dialog>
  …
</dialog>

Wir können diese Baseline verbessern.

Traditionell hat ein Dialogfeld viel mit einem modalen Dialogfeld gemeinsam und die Namen sind oft austauschbar. Ich habe das Dialogelement hier sowohl für kleine als auch für ganze Seitendialogfelder (Mega) verwendet. Ich habe sie „Mega“ und „Mini“ genannt. Beide Dialoge wurden leicht an unterschiedliche Anwendungsfälle angepasst. Ich habe das modal-mode-Attribut hinzugefügt, damit 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 Mega-Dialogfelds im hellen und dunklen Design

Nicht immer, aber in der Regel werden Dialogfelder verwendet, um Interaktionsinformationen zu erfassen. Formulare innerhalb von Dialogelementen sind aufeinander abgestimmt. Es ist empfehlenswert, den Dialoginhalt in einem Formularelement zu umschließen, damit JavaScript auf die vom Nutzer eingegebenen Daten zugreifen kann. Außerdem können Schaltflächen in einem Formular mit method="dialog" 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 enthält drei Elemente im Formular: <header>, <article> und <footer>. Diese dienen als semantische Container sowie Stilziele für die Darstellung des Dialogfelds. Die Überschrift gibt den Titel des Modals an und bietet eine Schaltfläche zum Schließen. Der Artikel bezieht sich auf Formulareingaben und -informationen. Die Fußzeile enthält eine Reihe von Aktionsschaltflächen <menu>.

<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 onclick-Inline-Ereignishandler. Das Attribut autofocus erhält den Fokus, wenn das Dialogfeld geöffnet wird. Ich empfehle, 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, es fehlt nur ein <header>-Element. So kann er kleiner und platzsparender sein.

<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 Dialogfeldelement bildet eine solide Grundlage für ein Full-Viewport-Element, 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 Dialogfeld bietet sehr gute integrierte Bedienungshilfen. Anstatt diese Funktionen wie üblich hinzuzufügen, sind viele bereits vorhanden.

Fokus wird wiederhergestellt

Wie wir es im Artikel Eine Seitenleiste erstellen per Hand gemacht haben, ist es wichtig, dass beim Öffnen und Schließen eines Elements der Fokus richtig auf den entsprechenden Schaltflächen zum Öffnen und Schließen liegt. Wenn die Seitenleiste geöffnet wird, liegt der Fokus auf der Schaltfläche „Schließen“. Wenn die Schaltfläche zum Schließen gedrückt wird, wird der Fokus wieder auf die Schaltfläche gelegt, über die das Fenster geöffnet wurde.

Beim Dialogfeld ist dies das integrierte Standardverhalten:

Wenn Sie das Dialogfeld ein- und ausblenden möchten, ist diese Funktion leider nicht mehr verfügbar. Im JavaScript-Abschnitt werde ich diese Funktion wiederherstellen.

Fokus festlegen

Das Dialogfeld-Element verwaltet inert für Sie im Dokument. Vor inert wurde JavaScript verwendet, um zu prüfen, ob der Fokus ein Element verlässt. In diesem Fall wurde er abgefangen und wiederhergestellt.

Unterstützte Browser

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 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 Dialogfeld dem ersten fokussierbaren Element im Dialogfeld-Markup den Fokus zu. Wenn dies nicht das beste Element für den Nutzer ist, verwende das Attribut autofocus. Wie bereits erwähnt, halte ich es für eine Best Practice, diese Meldung auf der Schaltfläche „Abbrechen“ und nicht auf der Schaltfläche „Bestätigen“ zu platzieren. So wird sichergestellt, dass die Bestätigung bewusst und nicht versehentlich erfolgt.

Mit der Esc-Taste schließen

Es ist wichtig, dass dieses potenziell störende Element leicht geschlossen werden kann. Glücklicherweise wird die Escape-Taste vom Dialogfeldelement für Sie verarbeitet, sodass Sie sich nicht um die Orchestration kümmern müssen.

Stile

Es gibt eine einfache und eine schwierige Möglichkeit, das Dialogfeld zu stylen. Der einfache Weg besteht darin, die Anzeigeeigenschaft des Dialogfelds nicht zu ändern und mit den Einschränkungen zu arbeiten. Ich benutzerdefinierte Animationen zum Öffnen und Schließen des Dialogfelds und übernehme dabei unter anderem das Attribut display.

Styling 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. Dank dieser Importe kann ich mich darauf konzentrieren, den Dialog und die Demo anzupassen, ohne viele Stile zu benötigen, um sie zu unterstützen und gut aussehen zu lassen.

<dialog>-Element stylen

Inhaber der Anzeigeeigenschaft

Beim Standardverhalten zum Ein- und Ausblenden eines Dialogfeldelements wird die Anzeigeeigenschaft von block auf none umgestellt. Das bedeutet leider, dass es nicht ein- und ausgeblendet werden kann, sondern nur ein-. Ich möchte eine Animation sowohl ein- als auch aussteigen und der erste Schritt besteht darin, meine eigene display-Eigenschaft festzulegen:

dialog {
  display: grid;
}

Wenn Sie den Wert der Displayeigenschaft ändern und somit die Verantwortung dafür übernehmen, wie im obigen CSS-Snippet gezeigt, müssen eine beträchtliche Anzahl von Stilen verwaltet werden, um eine nutzerfreundliche Oberfläche zu ermöglichen. Der Standardstatus eines Dialogfelds ist 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;
}

Das Dialogfeld ist jetzt unsichtbar und kann nicht geöffnet werden. Später füge ich JavaScript hinzu, um das inert-Attribut im Dialogfeld zu verwalten und dafür zu sorgen, dass auch Nutzer mit Tastatur und Screenreader das ausgeblendete Dialogfeld nicht aufrufen können.

Dialogfeld ein adaptives Farbdesign geben

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

Mit color-scheme wird für Ihr Dokument ein vom Browser bereitgestelltes adaptives Farbdesign für helle und dunkle Systemeinstellungen ausgewählt. Ich wollte das Dialogfeld jedoch 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 verwende gerne Farbe, um die visuelle Darstellung von Ebenenoberflächen zu unterstützen. 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 zusätzliches Element für ein Dialogfeld, aber sie sind wirklich wichtig für ein überzeugendes und gut gestaltetes Dialogfeld.

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, den Text in max-inline-size auf eine lesbare Größe (--size-content-3 = 60ch) oder 90 % der Darstellungsbereichsbreite zu beschränken. So wird sichergestellt, dass das Dialogfeld auf einem Mobilgerät nicht von Rand zu Rand reicht und auf einem Desktop-Bildschirm nicht so breit ist, dass es schwer zu lesen 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? Die erste verwendet 80vh, eine physische Viewport-Einheit. Ich möchte den Dialog für internationale Nutzer möglichst flüssig gestalten. Deshalb verwende ich in der zweiten Deklaration die logische, neuere und nur teilweise unterstützte Einheit dvb, bis sie stabiler ist.

Positionierung des Mega-Dialogfelds

Um ein Dialogelement zu positionieren, sollten Sie es in zwei Teile unterteilen: 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 frei über diesem Hintergrund zentrieren und die Form annehmen, die der 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 für Mega-Dialogfelder auf Mobilgeräten

Bei kleinen Darstellungsbereichen gestalte ich dieses Vollbild-Modaldialogfeld etwas anders. Ich habe den unteren Rand auf 0 festgelegt, wodurch der Dialoginhalt am unteren Rand des Darstellungsbereichs angezeigt wird. Mit ein paar Stilanpassungen kann ich den Dialog in ein Aktionsmenü umwandeln, das näher an den Daumen der Nutzer liegt:

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

Screenshot der DevTools, die den Ränderabstand sowohl auf dem Computer als auch auf dem Mobilgerät überlagern, während das Mega-Dialogfeld geöffnet ist.

Positionierung des Mini-Dialogfelds

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 denke, sie geht über den Rahmen dieses Artikels hinaus. Ohne JavaScript wird das Mini-Dialogfeld genau wie das Mega-Dialogfeld in der Mitte des Bildschirms angezeigt.

Ein echter Blickfang

Verleihen Sie dem Dialogfeld zum Schluss das gewisse Etwas, damit es wie eine weiche Oberfläche aussieht, die weit über der Seite ragt. Die Weichheit wird durch das Abrunden der Ecken des Dialogfelds erreicht. Die Tiefe wird mit einer 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 den Hintergrund nur sehr dezent bearbeitet und dem Mega-Dialogfeld mit backdrop-filter lediglich einen Weichzeichnereffekt hinzugefügt:

Unterstützte Browser

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

Quelle

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

Ich habe auch einen Übergang für backdrop-filter festgelegt, in der Hoffnung, dass Browser in Zukunft den Übergang des Hintergrundelements zulassen werden:

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

Screenshot des Mega-Dialogfelds, das einen unscharfen Hintergrund mit bunten Avataren überlagert.

Stiloptionen

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

Scrollen einschränken

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

Normalerweise wäre overscroll-behavior meine übliche Lösung, aber laut Spezifikation hat sie keine Auswirkungen auf den Dialog, da es sich nicht um einen Scroll-Port handelt, d. h., es ist kein Scroller, sodass nichts verhindert werden muss. Ich könnte JavaScript verwenden, um auf die neuen Ereignisse aus diesem Leitfaden zu warten, z. B. „geschlossen“ und „geöffnet“, und overflow: hidden im Dokument aktivieren oder deaktivieren. Ich könnte auch warten, bis :has() in allen Browsern stabil ist:

Unterstützte Browser

  • Chrome: 105
  • Edge: 105.
  • Firefox: 121.
  • Safari: 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

Neben der Tatsache, dass es ein sehr wichtiges Element für die Erfassung der Interaktionsinformationen der Nutzer ist, verwende ich es hier auch, um die Kopf-, Fuß- und Artikelelemente zu layouten. Mit diesem Layout möchte ich das untergeordnete Artikelelement als scrollbaren Bereich artikulieren. Das erreiche ich mit grid-template-rows. Das Artikelelement hat 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.

Dialogfeld <header> stylen

Dieses Element dient dazu, einen Titel für den Dialoginhalt anzugeben und eine leicht zu findende Schaltfläche zum Schließen anzubieten. Außerdem wird ihm eine Oberflächenfarbe zugewiesen, damit es den Anschein hat, als befände es sich hinter dem Inhalt des Dialogartikels. 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 Informationen zum Flexbox-Layout über der Dialogkopfzeile

Stil der Schaltfläche „Überschrift schließen“ festlegen

Da in der Demo die Schaltflächen „Requisiten öffnen“ verwendet werden, wird die Schaltfläche „Schließen“ so angepasst, dass sie ein rundes Symbol enthält:

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.

Dialogfeld <article> stylen

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 dieses Artikelelement erreichen muss, 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 Fußzeile enthält Menüs mit Aktionsschaltflächen. Mit Flexbox wird der Inhalt an das Ende der Inline-Achse der Fußzeile ausgerichtet und dann ein Abstand hinzugefü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 mit Informationen zum Flexbox-Layout, die auf dem Fußzeilenelement eingeblendet sind

Das Element menu enthält die Aktionsschaltflächen für das Dialogfeld. Es verwendet ein flexibles Flexbox-Layout mit gap, um Platz zwischen den Schaltflächen zu schaffen. Menüelemente haben einen Abstand, z. B. ein <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 der Fußzeile

Animation

Dialogelemente werden oft animiert, da sie in das Fenster ein- und austreten. 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 Dialogfeld nur ein-, aber nicht ausgeblendet werden. Das liegt daran, dass der Browser die Property display für das Element umschaltet. Bisher wurde in der Anleitung immer „Raster“ als Anzeige festgelegt. Dadurch können Sie ein- und ausblenden.

Open Props bietet viele Keyframe-Animationen, die die Orchestration einfach und übersichtlich machen. Hier sind die Animationziele und der mehrschichtige Ansatz, den ich verwendet habe:

  1. „Eingeschränkte Bewegung“ ist der Standardübergang, bei dem die Deckkraft einfach ein- und ausgeblendet wird.
  2. Wenn die Bewegung in Ordnung ist, werden Folien- und Skalierungsanimationen hinzugefügt.
  3. Das responsive mobile Layout für das Mega-Dialogfeld ist so angepasst, dass es herausgeschoben werden kann.

Eine sichere und sinnvolle Standardüberleitung

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. Wenn Sie einen Übergang zwischen 0 % und 100 % festlegen möchten, geben Sie dem Browser an, wie lange und wie die Überblendung erfolgen soll:

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

Bewegung zum Übergang hinzufügen

Wenn der Nutzer Bewegungen akzeptiert, sollten sowohl die Mega- als auch die Minidialoge beim Öffnen nach oben wischen und beim Schließen herauszoomen. Das geht mit der prefers-reduced-motion-Medienabfrage und einigen Open Props:

@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 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 Ausblendungsanimation passt nicht gut zu diesem neuen Design. Wir können sie 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

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.

Schließen des Lichts hinzufügen

Diese Aufgabe ist unkompliziert und eine gute Ergänzung zu einem Dialogfeld, das nicht animiert wird. Die Interaktion wird durch das Beobachten von Klicks auf das Dialogelement und die Verwendung von Ereignis-Bubbling erreicht, um zu beurteilen, was angeklickt wurde. Die Funktion wird nur dann close(), 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 anderen JavaScript-Code abgerufen werden, um Informationen dazu zu erhalten, wie der Dialog geschlossen wurde. Außerdem habe ich jedes Mal, wenn ich die Funktion über verschiedene Schaltflächen aufrufe, Schließstrings angegeben, um meiner Anwendung Kontext zur Nutzerinteraktion zu geben.

Ereignisse zum Schließen und Schließen von Routen hinzufügen

Das Dialogfeld-Element hat ein Ereignis vom Typ „close“ (Schließen): Es wird sofort ausgelöst, wenn die Funktion „dialog“ close() aufgerufen wird. Da wir dieses Element animieren, ist es sinnvoll, Ereignisse vor und nach der Animation zu haben, um die Daten abzurufen oder das Dialogfeld zurückzusetzen. Hier verwende ich es, 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 eingereicht hat.

Erstellen Sie dazu zwei neue Ereignisse mit den Namen closing und closed. Anschließend warten Sie auf das integrierte Ereignis „close“ (Schließen) für das Dialogfeld. Lege hier inert für das Dialogfeld fest und sende das Ereignis closing. Als Nächstes müssen Sie warten, bis die Animationen und Übergänge im Dialogfeld abgeschlossen sind, und dann das Ereignis closed 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 in der Toast-Komponente verwendet wird, gibt ein Promise zurück, das auf dem Abschluss der Animations- und Übergangs-Promises basiert. Deshalb ist dialogClose eine async-Funktion. Sie kann dann das zurückgegebene Promise await und das Ereignis „geschlossen“ sicher verarbeiten.

Öffnende und geöffnete Ereignisse hinzufügen

Diese Ereignisse lassen sich nicht so einfach hinzufügen, da das integrierte Dialogfeld kein Ereignis zum Öffnen bereitstellt, wie es beim Schließen der Fall ist. Ich verwende einen MutationObserver, um Informationen zu den sich ändernden Attributen des Dialogfelds zu erhalten. In diesem Beobachter werde ich auf Änderungen am Attribut „offen“ achten und die benutzerdefinierten Ereignisse entsprechend verwalten.

Ähnlich wie bei den Ereignissen „closing“ und „closed“ erstellen Sie zwei neue Ereignisse mit den Namen opening und opened. Während wir zuvor auf das Ereignis „Dialog schließen“ gewartet haben, verwenden wir dieses Mal 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 Mutations-Observers wird aufgerufen, wenn die Dialogattribute geändert werden. Dabei wird die Liste der Änderungen als Array übergeben. Gehe die Attributänderungen durch und prüfe, ob attributeName geöffnet ist. Prüfen Sie als Nächstes, ob das Element das Attribut hat. So erfahren Sie, ob das Dialogfeld geöffnet wurde. Wenn es geöffnet wurde, entfernen Sie das inert-Attribut 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 -zuständen hinzugefügt und entfernt. Es kann hilfreich sein, Ereignisse oder Daten zu bereinigen, wenn ein Dialog entfernt wird.

Das ist mit einem anderen Mutationsbeobachter möglich. Anstatt Attribute eines Dialogfeldelements zu beobachten, sehen wir uns diesmal die untergeordneten Elemente des Body-Elements an und achten darauf, ob Dialogfelder 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 Rückruf des Mutationsbeobachters wird jedes Mal aufgerufen, wenn dem Textkörper des Dokuments Elemente 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 Speicher freizugeben, und das benutzerdefinierte Ereignis „entfernt“ wird gesendet.

Entfernen des Attributs „Laden“

Damit die Dialogfeldanimation beim Hinzufügen zur Seite oder beim Laden der Seite nicht wiedergegeben wird, wurde dem Dialogfeld ein Ladeattribut hinzugefügt. Im folgenden Script wird gewartet, bis die Dialogfeldanimationen abgeschlossen sind, und dann wird das Attribut entfernt. Jetzt kann der Dialogfeldbereich animiert werden und wir haben eine ansonsten ablenkende Animation effektiv ausgeblendet.

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

Weitere Informationen zum Verhindern von Keyframe-Animationen beim Laden der Seite

Zusammen

Hier ist dialog.js in voller Länge, nachdem wir jeden Abschnitt einzeln erklärt 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 wird aufgerufen und erhält ein Dialogfeldelement, 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)

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

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 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 Dialogfeld erstellt habe, verwende ich dieses Ereignis und die Formulardaten, um der Liste ein neues Avatarelement 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 lässt sich die Nutzererfahrung noch besser steuern.

Hinweis dialog.returnValue: Dies ist der Schließstring, der übergeben wird, wenn das Dialog-close()-Ereignis aufgerufen wird. Im Ereignis dialogClosed ist es wichtig zu wissen, ob das Dialogfeld geschlossen, abgebrochen oder bestätigt wurde. Wenn die Bestätigung erfolgt, ruft das Script die Formularwerte ab und setzt das Formular zurück. Das Zurücksetzen ist nützlich, damit das Dialogfeld beim nächsten Mal leer ist und eine neue Einreichung möglich ist.

Fazit

Wie würden Sie es machen?

Lassen Sie uns unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können.

Erstelle eine Demo, tweete mir Links und ich füge sie unten in den Abschnitt „Community-Remixe“ hinzu.

Community-Remixe

Ressourcen