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 Ihnen zeigen, wie Sie mit dem Element <dialog> farbadaptiv, responsiv und barrierefrei nutzbare Mini- und Mega-Modalfenster erstellen. 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 <dialog>-Element 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. Daher 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 Dialogfeld sowohl für kleine Pop-up-Dialogfelder (Mini) als auch für Dialogfelder auf einer ganzen Seite (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 im dunklen Design

Nicht immer, aber in der Regel werden Dialogfelder verwendet, um Interaktionsinformationen zu erfassen. Formulare in Dialogelementen gehören zusammen. 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>. Sie dienen als semantische Container sowie als 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.

Mini-Dialogfeld

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 Elemente können für sehr interessante und leistungsstarke Interaktionen auf Ihrer Website oder in Ihrer App sorgen.

Bedienungshilfen

Das Dialogfeld bietet sehr gute integrierte Bedienungshilfen. Anstatt diese Funktionen wie üblich hinzuzufügen, sind viele bereits vorhanden.

Fokus wiederherstellen

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, mit der 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 inert können alle Teile des Dokuments „eingefroren“ werden, sodass sie nicht mehr als Fokusziele ausgewählt werden können und nicht mehr mit der Maus interaktiv sind. Anstatt den Fokus zu fixieren, wird er auf den einzigen interaktiven Teil des Dokuments gelenkt.

Element öffnen und automatischen Fokus darauf setzen

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, verwenden Sie 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 der Escape-Schlüssel vom Dialogfeldelement für Sie verarbeitet, sodass Sie sich nicht um die Orchestrierung 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 gehe den harten Weg, um benutzerdefinierte Animationen zum Öffnen und Schließen des Dialogfelds bereitzustellen, die display-Property zu übernehmen und mehr.

Styling mit offenen Requisiten

Um die adaptiven Farben und die Designkonsistenz insgesamt zu beschleunigen, habe ich meine CSS-Variablenbibliothek Open Props verwendet. Zusätzlich zu den kostenlos bereitgestellten Variablen importiere ich auch eine Normalisierungsdatei und einige Schaltflächen, die Open Props als optionale Importe anbietet. 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 zu none umgeschaltet. Das bedeutet leider, dass es nicht herein- und herausgezoomt werden kann, sondern nur herein. Ich möchte sowohl das Ein- als auch das Ausblenden animieren. Als Erstes muss ich meine eigene display-Property festlegen:

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 ordnungsgemäße Nutzererfahrung zu ermöglichen. Der Standardstatus eines Dialogfelds ist geschlossen. Sie können diesen Status visuell darstellen und verhindern, dass das Dialogfeld mit den folgenden Stilen interagiert:

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 Oberflächenfarben 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 die hellen und dunklen Systemeinstellungen anpassen, ähnlich wie bei der Verwendung der 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). Wenn Sie eine Ebene darüber einfügen möchten, 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 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.

Responsive Dialogfeldgröße

Standardmäßig wird die Größe des Dialogfelds an den Inhalt angepasst. Das ist in der Regel sehr praktisch. 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 ein max-block-size hinzu, damit das Dialogfeld nicht die Höhe der Seite ü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;
}

Sehen Sie, 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 Dialogfeld zu positionieren, sollten Sie es in zwei Teile unterteilen: den Vollbildhintergrund und den Dialogfeldcontainer. Der Hintergrund muss alles abdecken und einen Schatteneffekt erzeugen, der unterstützt, dass dieses Dialogfeld im Vordergrund ist und die Inhalte dahinter nicht zugänglich sind. Der Dialogcontainer kann sich kostenlos über diesem Hintergrund zentrieren und die Form annehmen, die der Inhalt erfordert.

Bei den folgenden Stilen wird das Dialogfeld am Fenster fixiert, bis es die gesamte Fensterfläche einnimmt, und der Inhalt wird mit margin: auto zentriert:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Stile für Mega-Dialogfelder auf Mobilgeräten

Auf kleinen Darstellungsbereichen habe ich dieses Mega-Modalfenster auf der ganzen Seite etwas anders gestaltet. 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

Bei einem größeren Ansichtsbereich, z. B. auf einem Desktop-Computer, habe ich die Minidialoge über dem Element platziert, das sie aufgerufen hat. 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

Zum Schluss verleihen Sie dem Dialogfeld etwas Flair, damit es wie eine weiche Oberfläche wirkt, die weit über der Seite schwebt. Die Weichheit wird durch das Abrunden der Ecken des Dialogfelds erreicht. Die Tiefe wird mit einem der sorgfältig gestalteten Schatten-Assets von Open Props erreicht:

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

Pseudo-Element „backdrop“ 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 einen Übergang für das Hintergrundelement 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“, weil er mehr mit meiner Dialogelement-Demo als mit dem Dialogelement im Allgemeinen zu tun hat.

Scrollen einschränken

Wenn das Dialogfeld angezeigt wird, kann der Nutzer weiterhin auf der Seite dahinter 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 wichtigen Rolle bei der Erfassung der Interaktionsinformationen der Nutzer verwende ich sie hier auch, um die Kopf-, Fuß- und Artikelelemente zu layouten. Mit diesem Layout möchte ich das Artikel-Unterelement als scrollbaren Bereich gestalten. Das erreiche ich mit grid-template-rows. Das Artikelelement hat 1fr und das Formular selbst hat dieselbe maximale Höhe wie das Dialogelement. Durch Festlegen dieser festen Höhe und festen Zeilengröße kann das Artikelelement eingegrenzt und bei Überlauf gescrollt werden:

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

Screenshot der DevTools, in denen die Informationen zum Rasterlayout über die Zeilen gelegt werden

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 Abständen zu den Rändern sowie einigen Rändern und Lücken, um dem Titel und den Schaltflächen zum Schließen 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 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 Informationen zur Größe und zum Abstand der Schaltfläche zum Schließen der Überschrift

Dialogfeld <article> stylen

Das Artikelelement hat in diesem Dialogfeld eine besondere Rolle: Es ist ein Bereich, der bei einem hohen 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 Scrollleisten nur bei Bedarf angezeigt werden, und verwenden Sie overscroll-behavior: contain zum Scrollen. 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 Sie Dialogfeldern eine unterstützende Bewegung für diesen Einstieg und Ausstieg geben, können sich Nutzer leichter im Fluss 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. So können Sie das Ein- und Ausblenden animieren.

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 Animationen für das Schwenken und Skalieren 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

Open Props bietet zwar Keyframes für das Ein- und Ausblenden, ich bevorzuge jedoch diesen mehrschichtigen Ansatz für Übergänge als Standard mit Keyframe-Animationen als möglichen Upgrades. Wir haben bereits die Sichtbarkeit des Dialogfelds mithilfe der Deckkraft gestaltet und je nach [open]-Attribut 1 oder 0 festgelegt. 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 zum Stil wurde der Mega-Dialogstil für Mobilgeräte so angepasst, dass er eher einer Infoseite ähnelt, als würde ein kleines Stück Papier von unten am Bildschirm nach oben geschoben und dort noch befestigt sein. Die Ausblendungsanimation passt nicht gut zu diesem neuen Design. Wir können sie mit ein paar 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 können Sie viele 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 resultieren aus dem Wunsch nach einer einfachen Schließung (durch Klicken auf den Hintergrund des Dialogfelds), einer Animation und einigen zusätzlichen Ereignissen, um die Formulardaten besser zu erfassen.

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 den Dialog auf inert 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“ verarbeiten.

Eröffnungs- und Termin 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. Senden Sie abschließend, ähnlich wie bei den Ereignissen „closing“ und „closed“, das Ereignis „opening“ sofort, warten Sie, bis die Animationen abgeschlossen sind, und senden Sie dann das Ereignis „opened“.

Entferntes Ereignis 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 der nodeName eines Dialogs. 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 „Ladezeit“

Um zu verhindern, dass die Dialogfeldanimation beim Hinzufügen zur Seite oder beim Laden der Seite wieder beendet 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 kostenlos 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 Problem, wie Sie Keyframe-Animationen beim Laden der Seite verhindern

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

dialog.js-Modul 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 wurden die beiden Dialogfelder mit einer leichten Schließung, Fehlerkorrekturen beim Laden von Animationen und mehr Ereignissen aktualisiert.

Neue benutzerdefinierte Ereignisse

Jedes aktualisierte Dialogfeld 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 Dialogfeld erstellt habe, verwende ich dieses Ereignis und die Formulardaten, um der Liste ein neues Avatarelement hinzuzufügen. Das Timing ist gut, da die Schließanimation des Dialogfelds abgeschlossen ist und dann einige Scripts den neuen Avatar animieren. Dank der neuen Ereignisse kann die Nutzererfahrung reibungsloser gestaltet werden.

Hinweis dialog.returnValue: Dieser enthält den 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 das 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.

Remixe der Community

Ressourcen