Podstawowe informacje o tym, jak tworzyć dostosowujące się do kolorów, responsywne i dostępne małe i duże okna modalne za pomocą elementu <dialog>
.
W tym poście chcę podzielić się moimi przemyśleniami na temat tworzenia dostosowujących się do kolorów, responsywnych i dostępnych mini- i mega-modalnych okien za pomocą elementu <dialog>
.
Wypróbuj wersję demonstracyjną i wyświetl kod źródłowy.
Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:
Przegląd
Element
<dialog>
doskonale nadaje się do wyświetlania informacji kontekstowych lub działań na stronie. Zastanów się, kiedy użytkownik może skorzystać z działania na tej samej stronie zamiast działania na wielu stronach, np. gdy formularz jest mały lub jedynym działaniem wymaganym od użytkownika jest potwierdzenie lub anulowanie.
Element <dialog>
stał się ostatnio stabilny w różnych przeglądarkach:
Okazało się, że w tym elemencie brakuje kilku rzeczy, więc w tym wyzwaniu dotyczącym interfejsu GUI dodaję oczekiwane elementy związane z wrażeniami programisty: dodatkowe zdarzenia, zamykanie przez kliknięcie poza elementem, niestandardowe animacje oraz typy mini i mega.
Znacznik
Podstawowe elementy <dialog>
są skromne. Element zostanie automatycznie ukryty i ma wbudowane style, które umożliwiają nakładanie treści.
<dialog>
…
</dialog>
Możemy ulepszyć ten punkt odniesienia.
Element dialogu jest podobny do okna modalnego i często nazwy te są używane zamiennie. Użyłem tutaj elementu dialogu zarówno w przypadku małych okien dialogowych (mini), jak i okien dialogowych na całą stronę (mega). Nazwałem je mega i mini, a oba okna dialogowe zostały nieco dostosowane do różnych zastosowań.
Dodałem atrybut modal-mode
, aby umożliwić Ci określenie typu:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Nie zawsze, ale zwykle elementy okna dialogowego służą do zbierania informacji o interakcjach. Formularze w elementach okna dialogowego są ze sobą powiązane.
Warto umieścić zawartość okna w elemencie formularza, aby JavaScript mógł uzyskać dostęp do danych wprowadzonych przez użytkownika. Ponadto przyciski w formularzu korzystającym z method="dialog"
mogą zamykać okno bez użycia JavaScriptu i przekazywać dane.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Okno Mega
Megaokno dialogowe zawiera 3 elementy w formularzu: <header>
, <article>
i <footer>
.
Służą one jako kontenery semantyczne, a także jako cele stylu do prezentacji okna. Nagłówek zawiera tytuł okna modalnego i przycisk zamykania. Artykuł dotyczy danych wejściowych i informacji w formularzu. W stopce znajduje się <menu>
przycisków polecenia.
<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>
Pierwszy przycisk menu ma
autofocus
i onclick
wbudowany moduł obsługi zdarzeń. Atrybut autofocus
zostanie zaznaczony po otwarciu okna, a najlepszym rozwiązaniem jest umieszczenie go na przycisku anulowania, a nie na przycisku potwierdzenia. Dzięki temu potwierdzenie jest celowe, a nie przypadkowe.
Miniokno
Miniokno jest bardzo podobne do megaokna, brakuje w nim tylko elementu <header>
. Dzięki temu jest mniejszy i bardziej dopasowany.
<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>
Element dialogu stanowi solidną podstawę dla elementu zajmującego cały obszar widoczny, który może zbierać dane i informacje o interakcjach użytkownika. Te podstawowe elementy mogą zapewnić bardzo ciekawe i skuteczne interakcje w Twojej witrynie lub aplikacji.
Ułatwienia dostępu
Element okna ma bardzo dobre wbudowane ułatwienia dostępu. Zamiast dodawać te funkcje, jak zwykle to robię, wiele z nich jest już dostępnych.
Przywracanie ostrości
Podobnie jak w przypadku ręcznego tworzenia komponentu paska bocznego, ważne jest, aby otwieranie i zamykanie elementu powodowało przeniesienie fokusu na odpowiednie przyciski otwierania i zamykania. Po otwarciu panelu bocznym fokus jest ustawiany na przycisku zamykania. Po naciśnięciu przycisku zamykania fokus wraca do przycisku, który otworzył okno.
W przypadku elementu okna jest to wbudowane zachowanie domyślne:
Jeśli chcesz animować pojawianie się i znikanie okna, ta funkcja nie będzie działać. W sekcji JavaScript przywrócę tę funkcję.
Ostrość z wyprzedzeniem
Element okna dialogowego zarządza
inert
w dokumencie. Przed wprowadzeniem inert
język JavaScript był używany do śledzenia, czy fokus opuszcza element. W takim przypadku kod przechwytywał fokus i przywracał go.
Po inert
dowolne części dokumentu mogą zostać „zamrożone”, co oznacza, że nie będą już celem zaznaczenia ani nie będą interaktywne w przypadku użycia myszy. Zamiast blokować fokus, jest on kierowany do jedynej interaktywnej części dokumentu.
Otwieranie elementu i automatyczne ustawianie na nim ostrości
Domyślnie element okna dialogowego przypisuje fokus do pierwszego elementu, który można zaznaczyć, w znacznikach okna dialogowego. Jeśli nie jest to najlepszy element domyślny dla użytkownika, użyj atrybutu autofocus
. Jak już wspominałem, zalecam umieszczenie tego komunikatu na przycisku anulowania, a nie na przycisku potwierdzenia. Dzięki temu potwierdzenie jest świadome, a nie przypadkowe.
Zamykanie za pomocą klawisza Escape
Ważne jest, aby można było łatwo zamknąć ten potencjalnie przeszkadzający element. Na szczęście element okna dialogowego obsłuży klawisz Escape, zwalniając Cię z obowiązku koordynacji.
Style
Istnieje łatwy i trudny sposób na stylowanie elementu okna dialogowego. Łatwiejsze rozwiązanie polega na tym, że nie zmieniasz właściwości wyświetlania okna i akceptujesz jego ograniczenia. Wybieram trudniejszą ścieżkę, aby zapewnić niestandardowe animacje otwierania i zamykania okna, przejmując kontrolę nad właściwością display
i nie tylko.
Stylizowanie za pomocą Open Props
Aby przyspieszyć dostosowywanie kolorów i zapewnić spójność projektu, bez skrupułów wykorzystałem bibliotekę zmiennych CSS Open Props. Oprócz bezpłatnych zmiennych importuję też plik normalize i kilka przycisków, które Open Props udostępnia jako opcjonalne importy. Dzięki tym importom mogę skupić się na dostosowywaniu okna i wersji demonstracyjnej, nie potrzebując wielu stylów, aby je obsługiwać i zapewnić im dobry wygląd.
Stylizowanie elementu <dialog>
Własność miejsca docelowego
Domyślne zachowanie elementu okna dialogowego polegające na wyświetlaniu i ukrywaniu przełącza właściwość display z block
na none
. Oznacza to, że nie można animować jej pojawiania się i znikania, a tylko pojawiania się. Chcę animować zarówno pojawianie się, jak i znikanie elementu, a pierwszym krokiem jest ustawienie własnej właściwości display:
dialog {
display: grid;
}
Zmiana wartości właściwości display, a tym samym jej przejęcie, jak pokazano w powyższym fragmencie kodu CSS, wymaga zarządzania znaczną liczbą stylów, aby zapewnić użytkownikom odpowiednią wygodę. Po pierwsze, domyślny stan okna to zamknięcie. Ten stan możesz przedstawić wizualnie i zapobiec interakcjom z oknem za pomocą tych stylów:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Teraz okno jest niewidoczne i nie można z niego korzystać, gdy jest zamknięte. Później dodam kod JavaScript, aby zarządzać atrybutem inert
w oknie, dzięki czemu użytkownicy klawiatury i czytników ekranu również nie będą mogli dotrzeć do ukrytego okna.
Nadawanie okienku adaptacyjnego motywu kolorystycznego
Chociaż color-scheme
włącza w dokumencie adaptacyjny motyw kolorystyczny dostarczany przez przeglądarkę, który dostosowuje się do jasnych i ciemnych ustawień systemu, chciałem bardziej dostosować element okna. Open Props udostępnia kilka kolorów powierzchni, które automatycznie dostosowują się do preferencji systemu dotyczących trybu jasnego i ciemnego, podobnie jak w przypadku używania color-scheme
. Świetnie nadają się do tworzenia warstw w projekcie. Uwielbiam używać kolorów, aby wizualnie wspierać wygląd powierzchni warstw. Kolor tła to var(--surface-1)
. Aby umieścić element na tej warstwie, użyj 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);
}
}
Później dodamy więcej kolorów adaptacyjnych dla elementów podrzędnych, takich jak nagłówek i stopka. Uważam je za dodatkowe w przypadku elementu okna, ale są one bardzo ważne w tworzeniu atrakcyjnego i dobrze zaprojektowanego okna.
Elastyczne rozmiary okien
Domyślnie okno dialogowe przekazuje swój rozmiar do treści, co jest zwykle bardzo przydatne. Moim celem jest ograniczenie max-inline-size
do czytelnego rozmiaru (--size-content-3
= 60ch
) lub 90% szerokości widocznego obszaru. Dzięki temu okno nie będzie zajmować całego ekranu urządzenia mobilnego ani nie będzie tak szerokie na ekranie komputera, że trudno będzie je przeczytać. Następnie dodaję
max-block-size
tak, aby okno nie przekraczało wysokości strony. Oznacza to również, że musimy określić, gdzie znajduje się obszar przewijania okna, jeśli jest to wysoki element okna.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Zwróć uwagę, że max-block-size
występuje 2 razy. Pierwszy z nich używa jednostki 80vh
, czyli fizycznej jednostki widocznego obszaru. Chcę, aby okno dialogowe było umieszczone w przepływie względnym, dlatego w przypadku użytkowników z innych krajów używam w drugiej deklaracji jednostki dvb
, która jest nowsza, logiczna i tylko częściowo obsługiwana, ale w przyszłości będzie bardziej stabilna.
Pozycjonowanie megaokna
Aby ułatwić pozycjonowanie elementu okna, warto podzielić go na 2 części: tło pełnoekranowe i kontener okna. Tło musi zakrywać wszystko, zapewniając efekt cienia, który potwierdza, że okno dialogowe jest na pierwszym planie, a treści znajdujące się za nim są niedostępne. Kontener okna dialogowego może być wyśrodkowany na tym tle i przyjmować dowolny kształt wymagany przez jego zawartość.
Poniższe style przytwierdzają element okna do okna, rozciągając go do każdego rogu, i używają margin: auto
do wyśrodkowania treści:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Style dużych okien na urządzeniach mobilnych
Na małych obszarach wyświetlania stylizuję ten pełnoekranowy mega modal nieco inaczej. Ustawiam dolny margines na 0
, co powoduje, że zawartość okna dialogowego jest wyświetlana u dołu obszaru widocznego na ekranie. Wystarczy kilka zmian stylu, aby przekształcić okno w arkusz działań, który będzie bliżej kciuków użytkownika:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Pozycjonowanie minidialogu
W przypadku większego obszaru wyświetlania, np. na komputerze stacjonarnym, zdecydowałem się umieścić miniokna nad elementem, który je wywołał. Aby to zrobić, potrzebuję JavaScriptu. Stosowaną przeze mnie technikę znajdziesz tutaj, ale uważam, że wykracza ona poza zakres tego artykułu. Bez JavaScriptu małe okno dialogowe pojawi się na środku ekranu, tak samo jak duże okno dialogowe.
Wyróżnij się
Na koniec dodaj do okna trochę stylu, aby wyglądało jak miękka powierzchnia znajdująca się daleko nad stroną. Miękkość uzyskuje się przez zaokrąglenie rogów okna. Głębię uzyskuje się za pomocą jednego ze starannie opracowanych właściwości cienia Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Dostosowywanie pseudoelementu tła
Tło zostało przeze mnie potraktowane bardzo delikatnie. Dodałem tylko efekt rozmycia za pomocą funkcji
backdrop-filter
w przypadku megaokna:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Dodałem też przejście do elementu backdrop-filter
, licząc na to, że w przyszłości przeglądarki będą obsługiwać przejścia w przypadku elementu tła:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Dodatki do stylizacji
Tę sekcję nazywam „dodatkami”, ponieważ bardziej dotyczy ona wersji demonstracyjnej elementu okna dialogowego niż samego elementu okna dialogowego.
Ograniczenie przewijania
Gdy okno dialogowe jest widoczne, użytkownik nadal może przewijać stronę za nim, co nie jest pożądane:
Zwykle używam rozwiązania overscroll-behavior
, ale zgodnie ze specyfikacją nie ma ono wpływu na okno, ponieważ nie jest ono portem przewijania, czyli nie jest przewijane, więc nie ma czego blokować. Mogę użyć JavaScriptu, aby śledzić nowe zdarzenia z tego przewodnika, takie jak „closed” i „opened”, i przełączać overflow: hidden
w dokumencie lub poczekać, aż :has()
będzie stabilny we wszystkich przeglądarkach:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Gdy otwarte jest duże okno, dokument HTML zawiera teraz element overflow: hidden
.
Układ <form>
Oprócz tego, że jest to bardzo ważny element do zbierania informacji o interakcjach użytkownika, używam go tutaj do rozmieszczania elementów nagłówka, stopki i artykułu. W tym układzie chcę przedstawić element podrzędny artykułu jako obszar z możliwością przewijania. Osiągam to za pomocą grid-template-rows
.
Element artykułu ma wartość 1fr
, a sam formularz ma taką samą maksymalną wysokość jak element okna. Ustawienie stałej wysokości i stałego rozmiaru wiersza umożliwia ograniczenie elementu artykułu i przewijanie go, gdy jego zawartość się nie mieści:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Stylizowanie okna dialogowego<header>
Jego zadaniem jest nadanie tytułu treściom okna i zapewnienie łatwego do znalezienia przycisku zamykania. Ma też kolor powierzchni, dzięki czemu wydaje się znajdować za treścią artykułu w oknie. Te wymagania prowadzą do utworzenia kontenera flexbox, elementów wyrównanych pionowo, które są rozmieszczone na krawędziach, oraz do dodania dopełnienia i odstępów, aby zapewnić tytułowi i przyciskom zamykania trochę miejsca:
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);
}
}
Stylizowanie przycisku zamykania nagłówka
W wersji demonstracyjnej używane są przyciski Open Props, więc przycisk zamknięcia jest dostosowany do okrągłej ikony umieszczonej na środku, jak w tym przykładzie:
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;
}
Stylizowanie okna dialogowego<article>
Element artykułu odgrywa w tym oknie specjalną rolę: jest to przestrzeń, którą można przewijać w przypadku wysokiego lub długiego okna.
Aby to osiągnąć, element formularza nadrzędnego ma określone maksymalne wartości, które ograniczają wysokość elementu artykułu. Ustaw overflow-y: auto
, aby paski przewijania wyświetlały się tylko w razie potrzeby, ogranicz przewijanie do tego elementu za pomocą overscroll-behavior: contain
, a resztę
dostosuj za pomocą niestandardowych stylów prezentacji:
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);
}
}
Stylizowanie okna dialogowego<footer>
Stopka zawiera menu przycisków poleceń. Flexbox służy do wyrównania treści do końca osi w wierszu stopki, a następnie do dodania odstępów, aby przyciski miały trochę miejsca.
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);
}
}
Stylizowanie menu stopki okna
Element menu
służy do umieszczania przycisków poleceń w oknie. Używa układu flexbox z zawijaniem i właściwości gap
, aby zapewnić odstępy między przyciskami. Elementy menu mają dopełnienie, np. <ul>
. Usuwam też ten styl, ponieważ nie jest mi potrzebny.
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;
}
Animacja
Elementy okna dialogowego są często animowane, ponieważ pojawiają się w oknie i z niego znikają. Dodanie do okien dialogowych animacji podczas ich pojawiania się i zamykania ułatwia użytkownikom orientację w procesie.
Zwykle element okna dialogowego można animować tylko w przypadku pojawiania się, a nie znikania. Dzieje się tak, ponieważ przeglądarka przełącza właściwość display
elementu. Wcześniej przewodnik ustawiał wyświetlanie na siatkę i nigdy nie ustawiał go na brak. Umożliwia to animowanie elementów podczas wchodzenia i wychodzenia.
Open Props zawiera wiele animacji klatek kluczowych, które ułatwiają i usprawniają tworzenie kompozycji. Oto cele animacji i zastosowane przeze mnie podejście warstwowe:
- Mniej animacji to domyślne przejście, proste zanikanie i pojawianie się.
- Jeśli ruch jest w porządku, dodawane są animacje przesuwania i skalowania.
- Elastyczny układ mobilny w przypadku dużego okna jest dostosowany do wysuwania.
Bezpieczne i sensowne domyślne przejście
Biblioteka Open Props zawiera klatki kluczowe do efektów pojawiania się i znikania, ale wolę to warstwowe podejście do przejść jako domyślne, z animacjami klatek kluczowych jako potencjalnymi ulepszeniami. Wcześniej stylizowaliśmy widoczność okna za pomocą właściwości opacity, która w zależności od atrybutu [open]
przyjmowała wartość 1
lub 0
. Aby przejść od 0% do 100%, określ, jak długo ma trwać przejście i jakiego rodzaju ma być wygładzanie:
dialog {
transition: opacity .5s var(--ease-3);
}
Dodawanie ruchu do przejścia
Jeśli użytkownik nie ma nic przeciwko animacji, oba okna (duże i małe) powinny wjeżdżać od dołu i oddalać się podczas zamykania. Możesz to zrobić za pomocą prefers-reduced-motion
zapytania o media i kilku właściwości 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;
}
}
Dostosowywanie animacji zakończenia do urządzeń mobilnych
W sekcji dotyczącej stylów wspomnieliśmy, że styl mega dialogu jest dostosowany do urządzeń mobilnych, aby przypominał arkusz działań. Wygląda to tak, jakby mały kawałek papieru wysunął się z dołu ekranu i nadal był do niego przymocowany. Animacja wyjścia z efektem powiększenia nie pasuje do tego nowego projektu, ale możemy ją dostosować za pomocą kilku zapytań o media i niektórych właściwości Open Props:
@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
Za pomocą JavaScriptu możesz dodać wiele elementów:
// 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
}
Te dodatki wynikają z chęci wprowadzenia możliwości lekkiego zamykania (kliknięcie tła okna), animacji i dodatkowych zdarzeń, które pozwolą lepiej określać czas uzyskiwania danych z formularza.
Dodawanie lekkiego odrzucania
Jest to proste zadanie, które świetnie uzupełnia element okna, który nie jest animowany. Interakcja jest osiągana przez obserwowanie kliknięć elementu okna i wykorzystanie propagacji zdarzeń do oceny, co zostało kliknięte. Nastąpi to tylko wtedy, gdy będzie to element najwyższego poziomu:close()
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Powiadomienie dialog.close('dismiss')
. Zdarzenie jest wywoływane i podawany jest ciąg tekstowy.
Ten ciąg znaków może być pobierany przez inne skrypty JavaScript, aby uzyskać informacje o tym, jak zostało zamknięte okno. Za każdym razem, gdy wywołuję funkcję z różnych przycisków, podaję też zbliżone ciągi znaków, aby zapewnić aplikacji kontekst interakcji użytkownika.
Dodawanie wydarzeń zamknięcia i zamkniętych
Element dialogu ma zdarzenie zamknięcia: jest ono emitowane natychmiast po wywołaniu funkcji close()
dialogu. Ponieważ animujemy ten element, warto mieć zdarzenia przed i po animacji, aby można było pobrać dane lub zresetować formularz okna. Używam go tutaj do zarządzania dodawaniem atrybutu inert
w zamkniętym oknie, a w wersji demonstracyjnej używam go do modyfikowania listy awatarów, jeśli użytkownik przesłał nowy obraz.
Aby to zrobić, utwórz 2 nowe zdarzenia o nazwach closing
i closed
. Następnie
nasłuchuj wbudowanego zdarzenia zamknięcia w oknie. Ustaw tutaj okno na inert
i wyślij zdarzenie closing
. Następne zadanie polega na poczekaniu, aż animacje i przejścia w oknie dialogowym zostaną zakończone, a następnie na wysłaniu zdarzenia closed
.
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))
Funkcja animationsComplete
, która jest też używana w tworzeniu komponentu powiadomienia, zwraca obietnicę na podstawie zakończenia animacji i obietnic przejścia. Dlatego dialogClose
jest funkcją asynchroniczną. Może wtedy await
zwróconą obietnicę i śmiało przejść do zamkniętego wydarzenia.
Dodawanie otwieranych i otwartych wydarzeń
Nie można ich łatwo dodać, ponieważ wbudowany element okna dialogowego nie udostępnia zdarzenia otwarcia, tak jak w przypadku zdarzenia zamknięcia. Używam interfejsu MutationObserver, aby dostarczać informacji o zmianach atrybutów okna. W tym obserwatorze będę śledzić zmiany atrybutu open i odpowiednio zarządzać zdarzeniami niestandardowymi.
Podobnie jak w przypadku zdarzeń zamykających i zamkniętych utwórz 2 nowe zdarzenia o nazwach opening
i opened
. Zamiast nasłuchiwać zdarzenia zamknięcia okna, tym razem użyjemy utworzonego obserwatora zmian, aby śledzić atrybuty okna.
…
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)
}
})
})
Funkcja wywołania zwrotnego obserwatora zmian zostanie wywołana, gdy zmienią się atrybuty okna, i przekaże listę zmian w postaci tablicy. Iteruj po zmianach atrybutów, szukając otwartego elementu attributeName
. Następnie sprawdź, czy element ma atrybut: dzięki temu dowiesz się, czy okno zostało otwarte. Jeśli okno zostało otwarte, usuń atrybut inert
i ustaw fokus na element, który wysyła żądanie autofocus
, lub na pierwszy element button
znaleziony w oknie. Na koniec, podobnie jak w przypadku zdarzeń closing i closed, od razu wyślij zdarzenie opening, poczekaj na zakończenie animacji, a następnie wyślij zdarzenie opened.
Dodawanie usuniętego wydarzenia
W aplikacjach na jednej stronie okna dialogowe są często dodawane i usuwane w zależności od tras lub innych potrzeb i stanu aplikacji. Może to być przydatne do czyszczenia zdarzeń lub danych po usunięciu okna.
Możesz to osiągnąć za pomocą innego obserwatora zmian. Tym razem zamiast obserwować atrybuty elementu okna dialogowego będziemy obserwować elementy podrzędne elementu body i sprawdzać, czy elementy okna dialogowego nie są usuwane.
…
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)
}
})
})
})
Funkcja zwrotna obserwatora zmian jest wywoływana za każdym razem, gdy do treści dokumentu dodawane są elementy podrzędne lub są z niej usuwane. Obserwowane mutacje dotyczą removedNodes
, które mają nodeName
okna. Jeśli okno zostało usunięte, zdarzenia kliknięcia i zamknięcia są usuwane, aby zwolnić pamięć, a wysyłane jest niestandardowe zdarzenie usunięcia.
Usuwanie atrybutu loading
Aby zapobiec odtwarzaniu animacji zamykania okna po dodaniu go do strony lub podczas ładowania strony, do okna dodano atrybut ładowania. Poniższy skrypt czeka na zakończenie animacji okna, a następnie usuwa atrybut. Teraz okno może być animowane podczas otwierania i zamykania, a my skutecznie ukryliśmy animację, która mogłaby rozpraszać uwagę.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Dowiedz się więcej o problemie z zapobieganiem animacjom klatek kluczowych podczas wczytywania strony.
Wszystkie razem
Oto pełna treść dialog.js
po wyjaśnieniu poszczególnych sekcji:
// 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')
}
Korzystanie z modułu dialog.js
Wyeksportowana funkcja z modułu oczekuje wywołania i przekazania elementu okna, do którego mają zostać dodane te nowe zdarzenia i funkcje:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
W ten sposób oba okna zostaną ulepszone o funkcję lekkiego zamykania, poprawki animacji wczytywania i więcej zdarzeń do wykorzystania.
Nasłuchiwanie nowych zdarzeń niestandardowych
Każdy ulepszony element okna może teraz nasłuchiwać 5 nowych zdarzeń, np. w ten sposób:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Oto 2 przykłady obsługi tych zdarzeń:
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()
}
}
W demonstracji, którą utworzyłem za pomocą elementu dialogu, używam zdarzenia zamknięcia i danych formularza, aby dodać do listy nowy element awatara. Jest to dobry moment, ponieważ okno dialogowe zakończyło animację wyjścia, a potem niektóre skrypty animują nowy awatar. Dzięki nowym zdarzeniom możesz łatwiej koordynować działania użytkowników.
Pole dialog.returnValue
: zawiera ciąg zamykający przekazywany podczas wywoływania zdarzenia close()
okna. W przypadku zdarzenia dialogClosed
ważne jest, aby wiedzieć, czy okno zostało zamknięte, anulowane czy potwierdzone. Jeśli to się potwierdzi, skrypt pobierze wartości formularza i zresetuje go. Resetowanie jest przydatne, ponieważ gdy okno dialogowe zostanie wyświetlone ponownie, będzie puste i gotowe do przesłania nowego zgłoszenia.
Podsumowanie
Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂
Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie.
Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.
Remiksy społeczności
- @GrimLink z dialogiem 3 w 1.
- @mikemai2awesome z fajnym remiksem, który nie zmienia właściwości
display
. - @geoffrich_ z użyciem Svelte i ładnego Svelte FLIP.
Zasoby
- Kod źródłowy na GitHubie
- Awatar Doodle