Dialogkomponente erstellen

Ein grundlegender Überblick darüber, wie Sie mit dem <dialog>-Element farbanpassungsfähige, responsive und barrierefreie Mini- und Mega-Modales erstellen.

In diesem Beitrag möchte ich meine Gedanken darüber teilen, wie man mit dem <dialog>-Element farbadaptive, responsive und barrierefreie Mini- und Mega-Modales erstellen kann. Demo ansehen und Quelle ansehen

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

Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:

Überblick

Das Element <dialog> eignet sich hervorragend für In-Page-Kontextinformationen oder -aktionen. Überlegen Sie, wann die Nutzererfahrung von derselben Seitenaktion anstatt einer mehrseitigen Aktion profitieren kann, z. B. weil das Formular klein ist oder die einzige Aktion, die der Nutzer erfordert, das Bestätigen oder Abbrechen ist.

Das <dialog>-Element ist seit Kurzem in allen Browsern stabil geworden:

Unterstützte Browser

  • 37
  • 79
  • 98
  • 15,4

Quelle

Ich habe festgestellt, dass bei dem Element ein paar Dinge fehlen. Daher füge ich in dieser GUI-Challenge die erwarteten Elemente für die Entwicklererfahrung hinzu: zusätzliche Ereignisse, das Ablehnen von Licht, benutzerdefinierte Animationen sowie einen Mini- und Mega-Typ.

Markup

Die wesentlichen Merkmale eines <dialog>-Elements sind bescheiden. Das Element wird automatisch ausgeblendet und verfügt über integrierte Stile, mit denen Sie Ihre Inhalte überlagern können.

<dialog>
  …
</dialog>

Diese Baseline können wir verbessern.

Üblicherweise hat ein Dialogelement viele gleiche Elemente wie ein Dialogelement und die Namen sind häufig austauschbar. Ich habe mir die Freiheit gelassen, das Dialogelement sowohl für kleine Pop-ups (Mini) als auch für Vollbild-Dialogfelder (Mega) zu verwenden. Ich habe sie Mega und Mini genannt, wobei beide Dialogfelder an unterschiedliche Anwendungsfälle angepasst sind. Ich habe ein modal-mode-Attribut hinzugefügt, mit dem Sie den Typ angeben können:

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

Screenshot der Mini- und der Mega-Dialogfelder mit hellem und dunklem Design.

Nicht immer, aber im Allgemeinen werden Dialogelemente verwendet, um einige Interaktionsinformationen zu erfassen. Formulare in Dialogfeldelementen sind so konzipiert, dass sie zusammenwirken. Es empfiehlt sich, den Dialog mit einem Formularelement umschließen zu lassen, damit JavaScript auf die vom Nutzer eingegebenen Daten zugreifen kann. Außerdem können Schaltflächen in einem Formular mit method="dialog" ein Dialogfeld ohne JavaScript schließen und Daten weitergeben.

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

Mega-Dialogfeld

Ein Mega-Dialogfeld hat drei Elemente im Formular: <header>, <article> und <footer>. Diese dienen als semantische Container und Stilziele für die Darstellung des Dialogfelds. Die Kopfzeile betitelt das Dialogfenster und enthält eine Schließen-Schaltfläche. Der Artikel dient der Eingabe von Formulareingaben und Informationen. Die Fußzeile enthält ein <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 onclick-Inline-Event-Handler. Das Attribut autofocus wird beim Öffnen des Dialogfelds fokussiert. Es hat sich bewährt, es auf die Schaltfläche „Abbrechen“ und nicht auf die Schaltfläche „Bestätigen“ zu platzieren. Dadurch wird eine absichtliche und nicht versehentliche Bestätigung sichergestellt.

Mini-Dialogfeld

Das Mini-Dialogfeld ist dem Mega-Dialog sehr ähnlich, es fehlt lediglich ein <header>-Element. Dadurch kann sie kleiner und direkter inline platziert 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 vollständigem Darstellungsbereich, das Daten und Nutzerinteraktionen erfassen kann. Diese Grundlagen können sehr interessante und wirkungsvolle Interaktionen auf Ihrer Website oder in Ihrer App ermöglichen.

Barrierefreiheit

Das Dialogelement hat eine sehr gute integrierte Barrierefreiheit. Statt diese Funktionen wie gewohnt hinzuzufügen, gibt es schon viele.

Fokus wird wiederhergestellt

Wie wir es bereits von Hand beim Erstellen einer Komponente für die Seitennavigation getan haben, ist es wichtig, dass beim ordnungsgemäßen Öffnen und Schließen von etwas die relevanten Schaltflächen zum Öffnen und Schließen in den Fokus rücken. Wenn diese seitliche Navigationsleiste geöffnet wird, liegt der Fokus auf der Schließen-Schaltfläche. Wenn die Schaltfläche zum Schließen gedrückt wird, ist wieder die Schaltfläche zum Öffnen fokussiert.

Das Dialogelement ist ein integriertes Standardverhalten:

Wenn Sie den Dialog ein- und ausblenden möchten, geht diese Funktion leider nicht mehr. Im JavaScript-Abschnitt stelle ich diese Funktion wieder her.

Fangfokus

Über das Dialogelement wird inert im Dokument für Sie verwaltet. Vor dem inert wurde mit JavaScript überwacht, ob ein Element fokussiert wurde und es abfängt und wieder zurücklegt.

Unterstützte Browser

  • 102
  • 102
  • 112
  • 15.5

Quelle

Nach inert können alle Teile des Dokuments eingefroren werden, sodass sie keine fokussierten Ziele mehr sind oder mit einer Maus interaktiv sind. Anstatt den Fokus einzuschränken, wird der Fokus auf den einzigen interaktiven Teil des Dokuments gelenkt.

Element öffnen und automatisch fokussieren

Standardmäßig weist das Dialogfeldelement den Fokus auf das erste fokussierbare Element im Dialogfeld-Markup zu. Wenn dies nicht das beste Element für den Nutzer ist, verwende das Attribut autofocus. Wie bereits erwähnt, empfiehlt es sich, dies auf die Schaltfläche „Abbrechen“ und nicht auf die Schaltfläche „Bestätigen“ zu platzieren. Dadurch wird sichergestellt, dass die Bestätigung absichtlich und nicht versehentlich erfolgt.

Mit Esc-Taste schließen

Es ist wichtig, dass sich dieses potenziell störende Element leicht schließen lässt. Glücklicherweise übernimmt das Dialogelement die Esc-Taste für Sie, sodass Sie nicht auf die Orchestrierung verzichten müssen.

Stile

Für die Gestaltung des Dialogelements gibt es einen einfachen Weg und einen festen Pfad. Der einfache Pfad besteht darin, die Anzeigeeigenschaft des Dialogfelds nicht zu ändern und mit seinen Einschränkungen zu arbeiten. Ich wähle den harten Pfad aus, um benutzerdefinierte Animationen zum Öffnen und Schließen des Dialogfelds bereitzustellen und die Eigenschaft display zu übernehmen.

Stile mit offenen Requisiten erstellen

Um adaptive Farben und ein einheitliches Design insgesamt zu beschleunigen, habe ich meine CSS-Variablenbibliothek Open Props hinzugefügt. Zusätzlich zu den kostenlos bereitgestellten Variablen importiere ich auch eine Normalisierungsdatei und einige Schaltflächen, die beide Open Props als optionale Importe bereitstellen. Durch diese Importe kann ich mich auf das Anpassen des Dialogfelds und der Demo konzentrieren, ohne dass viele Stile erforderlich sind, um sie zu unterstützen und gut aussehen zu lassen.

Stil für das <dialog>-Element festlegen

Inhaber der Display-Property

Durch das Standardverhalten zum Ein- und Ausblenden eines Dialogfeldelements wird die Anzeigeeigenschaft von block zu none geändert. Das bedeutet, dass sie nicht ein- und heraus animiert werden kann, sondern nur Ich möchte sowohl die In- als auch die Out-Animation animieren. Im ersten Schritt lege ich meine eigene display-Eigenschaft fest:

dialog {
  display: grid;
}

Wenn Sie, wie im CSS-Snippet oben gezeigt, den Wert der „display“-Eigenschaft ändern und somit Inhaber sind, müssen viele Stile verwaltet werden, um eine optimale Nutzererfahrung zu ermöglichen. Zuerst wird der Standardstatus eines Dialogfelds geschlossen. Sie können diesen Status visuell darstellen und verhindern, dass das Dialogfeld Interaktionen mit den folgenden Stilen empfängt:

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

Jetzt ist das Dialogfeld nicht sichtbar und kann nicht bearbeitet werden, wenn es nicht geöffnet ist. Später füge ich JavaScript hinzu, um das inert-Attribut im Dialogfeld zu verwalten. So sorgst du dafür, dass auch Nutzer von Tastatur und Screenreader nicht auf das ausgeblendete Dialogfeld zugreifen können.

Dialogfeld ein adaptives Farbdesign zuweisen

Mega-Dialog mit dem hellen und dem dunklen Design für die Oberflächenfarben.

Während in color-scheme für Ihr Dokument ein vom Browser bereitgestelltes adaptives Farbschema an helle und dunkle Systemeinstellungen aktiviert wird, wollte ich das Dialogelement noch weiter anpassen. Open Props bietet einige Oberflächenfarben, die sich automatisch an helle und dunkle Systemeinstellungen anpassen, ähnlich wie bei color-scheme. Sie eignen sich hervorragend zum Erstellen von Ebenen in einem Design und ich liebe es, Farben zu verwenden, um diese Darstellung von Ebenenoberflächen visuell zu unterstützen. Die Hintergrundfarbe ist var(--surface-1). Wenn Sie sie über dieser Ebene platzieren 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. Für ein Dialogelement sind sie besonders wichtig, aber für ein überzeugendes und gut gestaltetes Dialogdesign.

Größe von responsiven Dialogfeldern

Standardmäßig wird die Größe des Dialogfelds an den Inhalt delegiert, was in der Regel sehr positiv ist. Ich möchte max-inline-size auf eine lesbare Größe (--size-content-3 = 60ch) oder 90% der Breite des Darstellungsbereichs beschränken. Dadurch wird sichergestellt, dass das Dialogfeld auf einem Mobilgerät nicht Rand an Rand ist und auf einem Desktop-Bildschirm nicht so breit ist, dass es schwer zu lesen ist. Dann füge ich ein max-block-size-Element 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 ein großes Dialogelement ist.

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, wie ich max-block-size zweimal habe? Die erste verwendet 80vh, eine Einheit für den physischen Darstellungsbereich. Ich möchte, dass das Dialogfeld für internationale Nutzer innerhalb des relativen Ablaufs bleibt. Daher verwende ich die logische, neuere und nur teilweise unterstützte dvb-Einheit in der zweiten Deklaration für den Fall, dass sie stabiler wird.

Positionierung des Mega-Dialogfelds

Um das Positionieren eines Dialogelements zu erleichtern, ist es sinnvoll, seine zwei Teile aufzuschlüsseln: den Vollbildhintergrund und den Dialogfeldcontainer. Der Hintergrund muss alles abdecken und einen Schattierungseffekt bieten, um zu verdeutlichen, dass sich der Dialog im Vordergrund befindet und der Inhalt dahinter nicht zugänglich ist. Der Dialogcontainer kann sich vor diesem Hintergrund zentrieren und die Form des Dialogcontainers annehmen.

Mit den folgenden Stilen wird das Dialogelement an dem Fenster fixiert. Dabei wird es bis in jede Ecke gestreckt und mit margin: auto wird der Inhalt zentriert:

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

Bei kleinen Darstellungsbereichen kann ich dieses Megamodale-Vollbild ein wenig anders gestalten. Ich lege den unteren Rand auf 0 fest. Dadurch wird der Inhalt des Dialogfelds am unteren Rand des Darstellungsbereichs angezeigt. Mit ein paar Stilanpassungen kann ich das Dialogfeld in ein Aktionsblatt verwandeln, das näher an den Daumen der Nutzenden 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 Entwicklertools mit Überlagerung des Ränders im Mega-Dialogfeld für Desktop- und Mobilgeräte, während geöffnet ist.

Positionierung des Mini-Dialogfelds

Bei der Verwendung eines größeren Darstellungsbereichs, z. B. auf einem Desktop-Computer, habe ich mich dafür entschieden, die Mini-Dialogfelder über dem jeweiligen Element zu positionieren. Dazu benötige ich JavaScript. Hier finden Sie die von mir verwendete Methode. Ich denke jedoch, dass sie den Rahmen dieses Artikels sprengt. Ohne JavaScript erscheint das Mini-Dialogfeld wie ein Mega-Dialogfeld in der Mitte des Bildschirms.

Herausragend sein

Zuletzt fügen Sie dem Dialogfeld noch mehr Flair, damit es wie eine weiche Oberfläche weit über der Seite wirkt. Die Weichheit wird durch das Abrunden der Ecken des Dialoges erreicht. Die Tiefe wird mit einem der sorgfältig gestalteten Schattenrequisiten von Open Props erreicht:

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

Pseudoelement für den Hintergrund anpassen

Ich habe mich dafür entschieden, sehr locker mit dem Hintergrund zu arbeiten und dem Mega-Dialogfeld mit backdrop-filter nur einen Weichzeichnereffekt hinzuzufügen:

Unterstützte Browser

  • 76
  • 17
  • 103
  • 9

Quelle

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

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

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

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

Stile für Extras festlegen

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

Scrolleindämmung

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

Normalerweise wäre overscroll-behavior meine übliche Lösung, aber laut Spezifikation hat es keine Auswirkungen auf das Dialogfeld, da es kein Scroll-Port ist. Es ist also kein Scroller und nichts zu verhindern. Ich könnte mit JavaScript auf die neuen Ereignisse aus dieser Anleitung achten, z. B. „geschlossen“ und „geöffnet“, und im Dokument overflow: hidden aktivieren oder warten, bis :has() in allen Browsern stabil ist:

Unterstützte Browser

  • 105
  • 105
  • 121
  • 15,4

Quelle

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

Wenn jetzt ein Mega-Dialogfeld geöffnet wird, hat das HTML-Dokument overflow: hidden.

Das <form>-Layout

Das Element ist nicht nur ein sehr wichtiges Element, um Interaktionsinformationen der Nutzer zu erfassen, sondern auch die Elemente in der Kopfzeile, Fußzeile und Artikel anlegen. Mit diesem Layout möchte ich das Artikelunterelement als scrollbaren Bereich artikulieren. Dazu verwende ich grid-template-rows. Das Artikelelement erhält 1fr und das Formular selbst hat dieselbe maximale Höhe wie das Dialogelement. Wenn Sie diese feste Höhe und feste Zeilengröße festlegen, wird das Artikelelement eingeschränkt und scrollt, 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, die die Zeilen zum Rasterlayout überlagern

Dialog <header> gestalten

Die Rolle dieses Elements besteht darin, einen Titel für den Dialoginhalt und eine leicht zu findende Schließen-Schaltfläche bereitzustellen. Sie erhält auch eine Oberflächenfarbe, damit sie hinter dem Inhalt des Dialogartikels erscheinen kann. Diese Anforderungen führen zu einem Flexbox-Container, vertikal ausgerichteten Elementen mit Abstand zu ihren Rändern sowie einigen Abständen und Lücken, um den Schaltflächen für Titel und 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 Overlay-Informationen zum Flexbox-Layout in der Kopfzeile des Dialogfelds.

Stil der Schaltfläche zum Schließen der Kopfzeile

Da in der Demo die Schaltflächen „Open Props“ verwendet werden, wird die Schließen-Schaltfläche zu einer runden, mit einem Symbol gekennzeichneten Schaltfläche wie folgt angepasst:

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 Overlay-Informationen zu Größe und Abstand für die Schließen-Schaltfläche des Headers.

Dialog <article> gestalten

Das Artikelelement hat eine besondere Rolle in diesem Dialogfeld: Es ist ein Bereich, in dem gescrollt werden soll, wenn ein großes oder langes Dialogfeld angezeigt wird.

Um dies zu erreichen, hat das übergeordnete Formularelement selbst einige Maximalwerte festgelegt, die Einschränkungen dafür darstellen, dass das Artikelelement bei einer zu hohen Höhe nicht erreicht werden kann. Legen Sie overflow-y: auto fest, damit Bildlaufleisten nur bei Bedarf angezeigt werden. Sie können darin mit overscroll-behavior: contain scrollen und den 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 Funktion der Fußzeile besteht darin, Menüs mit Aktionsschaltflächen zu enthalten. Mithilfe der Flexbox wird der Inhalt am Ende der Inline-Achse der Fußzeile ausgerichtet und anschließend mit etwas Abstand, 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 Überlagerung der Flexbox-Layoutinformationen über das Fußzeilenelement

Das Element menu enthält die Aktionsschaltflächen für das Dialogfeld. Sie verwendet ein umbrechendes Flexbox-Layout mit gap, um Platz zwischen den Schaltflächen zu schaffen. Menüelemente haben einen Innenrand, z. B. <ul>. Ich entferne diesen Stil auch, da ich ihn nicht brauche.

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 Überlagerung der Flexbox-Informationen über die Elemente des Fußzeilenmenüs

Animation

Dialogelemente werden häufig animiert, weil sie in das Fenster ein- und aussteigen. Wenn Sie Dialoge mit einer unterstützenden Bewegung für den Ein- und Ausstieg versehen, können sich die Nutzer besser im Ablauf orientieren.

Normalerweise kann das Dialogelement nur ein- und nicht heraus animiert werden. Das liegt daran, dass der Browser die display-Eigenschaft des Elements umschaltet. Zuvor hat der Guide die Anzeige auf „Raster“ festgelegt, aber nie auf „Keine“. So lassen sich Animationen hin- und Heraus animieren.

Open Props enthält viele Keyframe-Animationen, die das Orchestrieren einfach und gut lesbar machen. Hier sind meine Animationsziele und mehrere Ebenen:

  1. Reduzierte Bewegung ist der Standardübergang, bei dem eine einfache Deckkraft ein- und ausgeblendet wird.
  2. Wenn Bewegung in Ordnung ist, werden Slide- und Skalierungsanimationen hinzugefügt.
  3. Das responsive mobile Layout für das Mega-Dialogfeld wird so angepasst, dass es herausrutscht.

Eine sichere und sinnvolle Standardumstellung

Open Props beinhaltet zwar Keyframes zum Ein- und Ausblenden, aber ich bevorzuge diesen mehrschichtigen Ansatz von Übergängen als Standard mit Keyframe-Animationen als mögliche Upgrades. Zuvor haben wir die Sichtbarkeit des Dialogfelds bereits mit Deckkraft gestaltet und 1 oder 0 (abhängig vom [open]-Attribut) orchestriert. Teilen Sie dem Browser für einen Übergang zwischen 0% und 100 % mit, wie lange und welche Art von Easing Sie benötigen:

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

Bewegung zum Übergang hinzufügen

Wenn der Nutzer mit Bewegungen einverstanden ist, sollten sowohl das Mega- als auch das Mini-Dialogfeld als Einstieg nach oben rutschen und beim Austritt heraus skaliert werden. Sie können dies mit der Medienabfrage 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;
  }
}

Exit-Animation für Mobilgeräte anpassen

Zu Beginn des Gestaltungsabschnitts wurde der Stil des Mega-Dialogfelds an Mobilgeräte angepasst, sodass es eher wie ein Aktionsblatt aussieht, als ob ein kleines Blatt Papier vom unteren Bildschirmrand nach oben geschoben und trotzdem am unteren Rand befestigt ist. Die Exit-Animation mit horizontaler Skalierung passt nicht gut zum neuen Design und wir können dies mit ein paar Medienabfragen und einigen Open Props anpassen:

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

JavaScript

Bei 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 einem einfachen Schließen (Klick auf den Dialoghintergrund), Animationen und einigen zusätzlichen Ereignissen für einen besseren Zeitpunkt für den Abruf der Formulardaten.

Hinzufügen von Lampe(n) schließen

Diese Aufgabe ist unkompliziert und eine gute Ergänzung für ein Dialogelement, das nicht animiert ist. Für die Interaktion werden Klicks auf das Dialogelement beobachtet und mithilfe von Ereignis-Bubbling ermittelt, worauf geklickt wurde. close() wird nur ausgeführt, wenn es das oberste Element ist:

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

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

Hinweis: dialog.close('dismiss'). Das Ereignis wird aufgerufen und ein String angegeben. Dieser String kann von anderem JavaScript abgerufen werden, um zu ermitteln, wie das Dialogfeld geschlossen wurde. Außerdem habe ich bei jedem Aufruf der Funktion über verschiedene Schaltflächen Schließzeichenfolgen angegeben, um für meine App Kontext zur Nutzerinteraktion bereitzustellen.

Hinzufügen von Abschluss- und geschlossenen Ereignissen

Das Dialogfeldelement enthält ein Schließereignis: Es wird sofort ausgelöst, wenn die Funktion close() des Dialogfelds aufgerufen wird. Da wir dieses Element animieren, ist es praktisch, Ereignisse vor und nach der Animation zu haben, um die Daten abzurufen oder das Dialogfeldformular zurückzusetzen. Ich verwende es hier, um das Hinzufügen des Attributs inert im geschlossenen Dialogfeld zu verwalten. In der Demo verwende ich sie, um die Avatarliste zu ändern, wenn der Nutzer ein neues Bild eingereicht hat.

Erstellen Sie dazu zwei neue Ereignisse mit den Namen closing und closed. Warten Sie dann im Dialogfeld auf das integrierte Schließereignis. Legen Sie hier das Dialogfeld auf inert fest und lösen Sie das Ereignis closing aus. Die nächste Aufgabe besteht darin, zu warten, bis die Animationen und Übergänge im Dialogfeld fertig ausgeführt werden, und dann das Ereignis closed auslösen.

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

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

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

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

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

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

Öffnende und geöffnete Termine hinzufügen

Diese Ereignisse lassen sich nicht so einfach hinzufügen, da das integrierte Dialogelement im Gegensatz zum Schließen kein Ereignis vom Typ „open“ bereitstellt. Ich verwende einen MutationObserver, um Einblicke in die Attribute des Dialogfelds zu geben, die sich ändern. In diesem Beobachter halte ich Änderungen am Attribut „open“ fest und verwalte die benutzerdefinierten Ereignisse entsprechend.

Ähnlich wie beim Start des Abschlussereignisses und des geschlossenen Ereignisses erstellst du zwei neue Ereignisse mit den Namen opening und opened. Während wir zuvor auf das Dialogschließereignis gewartet haben, verwenden Sie 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 Mutationsbeobachters wird aufgerufen, wenn die Dialogfeldattribute geändert werden. Dabei wird die Liste der Änderungen als Array zur Verfügung gestellt. Iterieren Sie die Attributänderungen und achten Sie darauf, dass das attributeName geöffnet ist. Prüfen Sie als Nächstes, ob das Element das Attribut hat oder nicht. Dadurch wird angegeben, ob das Dialogfeld geöffnet wurde. Entfernen Sie nach dem Öffnen das Attribut inert und setzen Sie den Fokus entweder auf ein Element, das autofocus anfordert, oder auf das erste button-Element, das im Dialogfeld gefunden wurde. Ähnlich wie beim schließenden und geschlossenen Ereignis wird das Eröffnungsereignis sofort ausgelöst, gewartet, bis die Animationen beendet sind, und dann das geöffnete Ereignis auslösen.

Entfernte Termine hinzufügen

In Single-Page-Anwendungen werden Dialogfelder oft basierend auf Routen, anderen Anforderungen und Status der Anwendung hinzugefügt und entfernt. Es kann hilfreich sein, Ereignisse oder Daten zu bereinigen, wenn ein Dialogfeld entfernt wird.

Dazu können Sie einen anderen Mutationsbeobachter verwenden. Anstatt Attribute für ein Dialogelement zu beobachten, beobachten wir dieses Mal die untergeordneten Elemente des body-Elements und achten darauf, ob Dialogelemente entfernt werden.

…
const dialogRemovedEvent = new Event('removed')

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

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

Der Callback des Mutationsbeobachters wird immer dann aufgerufen, wenn untergeordnete Elemente zum Textkörper des Dokuments hinzugefügt oder daraus entfernt werden. Die beobachteten spezifischen Mutationen beziehen sich auf removedNodes mit dem nodeName eines Dialogfelds. Wenn ein Dialogfeld entfernt wurde, werden die Ereignisse vom Typ „Klick“ und „Schließen“ entfernt, um Arbeitsspeicher freizugeben. Das benutzerdefinierte Ereignis „Remove“ wird ausgelöst.

Das Ladeattribut wird entfernt

Um zu verhindern, dass die Dialogfeldanimation die Exit-Animation abspielt, wenn sie der Seite oder beim Seitenaufbau hinzugefügt wird, wurde dem Dialogfeld ein Ladeattribut hinzugefügt. Das folgende Skript wartet, bis die Dialogfeldanimationen vollständig ausgeführt wurden, und entfernt dann das Attribut. Jetzt kann der Dialog kostenlos ein- und ausgeblendet werden. Außerdem haben wir eine ansonsten ablenkende Animation ausgeblendet.

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

Weitere Informationen zum Verhindern von Keyframe-Animationen beim Seitenaufbau

Ein perfektes Team

Hier ist die vollständige Übersicht über dialog.js, nachdem wir jeden Abschnitt im Detail 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 exportierte Funktion aus dem Modul erwartet, dass aufgerufen und ein Dialogelement übergeben wird, dem die folgenden neuen Ereignisse und Funktionen hinzugefügt werden sollen:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Jetzt werden die beiden Dialogfelder mit einem leichten Schließen, Fehlerkorrekturen beim Laden von Animationen und mehr Ereignissen aktualisiert, mit denen Sie arbeiten können.

Neue benutzerdefinierte Ereignisse beobachten

Jedes aktualisierte Dialogelement kann jetzt fünf neue Ereignisse beobachten:

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

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

MegaDialog.addEventListener('removed', dialogRemoved)

Hier zwei Beispiele für die Verarbeitung dieser Ereignisse:

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

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

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

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

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

Hinweis dialog.returnValue: Enthält den Schließstring, der beim Aufruf des Dialogs close()-Ereignis übergeben wird. Im dialogClosed-Ereignis ist es wichtig, zu wissen, ob das Dialogfeld geschlossen, abgebrochen oder bestätigt wurde. Wenn sie bestätigt wird, erfasst das Skript die Formularwerte und setzt das Formular zurück. Das Zurücksetzen ist nützlich, damit das Dialogfeld leer und bereit für eine neue Einreichung ist, wenn das Dialogfeld wieder angezeigt wird.

Fazit

Jetzt weißt du, wie ich es gemacht habe. Wie würdest du es erreichen? 🙂

Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können.

Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.

Community-Remixe

Ressourcen