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
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:
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>
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.
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
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;
}
}
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:
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;
}
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:
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;
}
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);
}
}
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;
}
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);
}
}
Dialog <footer>
gestalten
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);
}
}
Fußzeilenmenü des Dialogfelds gestalten
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;
}
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:
- Reduzierte Bewegung ist der Standardübergang, bei dem eine einfache Deckkraft ein- und ausgeblendet wird.
- Wenn Bewegung in Ordnung ist, werden Slide- und Skalierungsanimationen hinzugefügt.
- 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
- @GrimLink mit einem 3-in-1-Dialog.
- @mikemai2awesome mit einem ansprechenden Remix, bei dem die
display
-Eigenschaft nicht geändert wird. - @geoffrich_ mit Svelte und dem schönen Svelte FLIP-Politur.
Ressourcen
- Quellcode auf GitHub
- Doodle-Avatare