Tworzenie komponentu okna

Podstawowe informacje o tworzeniu elastycznych, elastycznych i łatwo dostępnych miniatur i megamodów z elementem <dialog>.

W tym poście chcę podzielić się swoimi przemyśleniami na temat tworzenia adaptacyjnych, elastycznych i łatwo dostępnych miniaturek i mega modałów z elementem <dialog>. Wypróbuj wersję demonstracyjną i wyświetl źródło.

Prezentacja dużych i małych okien dialogowych w jasnym i ciemnym motywie.

Jeśli wolisz film, oto wersja tego posta na YouTube:

Przegląd

Element <dialog> doskonale nadaje się do umieszczania na stronie informacji kontekstowych lub działań. Zastanów się, w jakich sytuacjach użytkownicy mogą skorzystać na tym samym działaniu na stronie zamiast na wielu stronach – np. dlatego, że formularz jest mały lub użytkownik musi tylko zatwierdzić lub anulować działanie.

Element <dialog> stał się ostatnio stabilny w różnych przeglądarkach:

Obsługa przeglądarek

  • 37
  • 79
  • 98
  • 15,4

Źródło

W elemencie brakowało kilku rzeczy, więc w tym wyzwaniu GUI dodam elementy związane z interfejsem programisty: dodatkowe zdarzenia, lekkie zamknięcie, niestandardowe animacje oraz typ mini i mega.

Markup

Podstawowe informacje o elemencie <dialog> są skromne. Element zostanie automatycznie ukryty i będzie miał wbudowane style nakładające się na treść.

<dialog>
  …
</dialog>

Możemy poprawić tę wartość bazową.

Tradycyjnie elementy okna są bardzo podobne do elementów modalnych i często ich nazwy są wymienne. Pozwolił mi on wykorzystać element okna zarówno w przypadku małych okien dialogowych (mini), jak i pełnoekranowych (mega). Nazwałam je „mega” i „mini”, przy czym oba okna były nieco przystosowane do różnych zastosowań. Dodaliśmy atrybut modal-mode, aby umożliwić Ci określenie typu:

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

Zrzut ekranu z miniaturami i dużymi oknami z motywami jasnymi i ciemnymi.

Do zbierania informacji o interakcji używane są zazwyczaj elementy dialogowe, nie zawsze. Formularze w elementach okna dialogowego muszą ze sobą współdziałać. Dobrym pomysłem jest dodanie elementu formularza do treści okna, tak aby kod JavaScript mógł uzyskać dostęp do danych wpisanych przez użytkownika. Ponadto przyciski w formularzu używającym atrybutu method="dialog" mogą zamykać okno bez 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>

Megaokno

Duże okno zawiera 3 elementy: <header>, <article> i <footer>. Służą one jako kontenery semantyczne, a także cele stylów do prezentacji okna. W nagłówku znajduje się przycisk zamykania. Ten artykuł zawiera informacje na temat formularzy. W stopce znajduje się <menu> z przyciskami działań.

<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 wbudowany moduł obsługi zdarzeń autofocus i onclick. Po otwarciu okna atrybut autofocus zostanie zaznaczony. Najlepiej jest go umieścić na przycisku anulowania, a nie na przycisku potwierdzenia. Dzięki temu potwierdzenie jest celowe i nieprzypadkowe.

Miniokno

Miniokno jest bardzo podobne do megaokna, ale brakuje w nim elementu <header>. Dzięki temu może być mniejszy i bardziej wbudowany.

<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 okna stanowi solidną podstawę dla elementu zajmującego cały widoczny obszar, który może zbierać dane i interakcje użytkowników. Dzięki temu możesz prowadzić ciekawe i skuteczne interakcje w witrynie lub aplikacji.

Ułatwienia dostępu

Element okna ma wbudowane ułatwienia dostępu. Zamiast dodawać je, jak zwykle, jest ich wiele dostępnych.

Przywracam ostrość

Tak jak w przypadku tworzenia komponentu nawigacji bocznej, ważne jest, aby poprawne otwieranie i zamykanie elementu koncentrowało się na odpowiednich przyciskach otwierania i zamykania. Po otwarciu panelu bocznego zaznaczenie zostanie umieszczone na przycisku zamykania. Po naciśnięciu przycisku zamykania fokus jest przywracany do przycisku, który go uruchomił.

W przypadku elementu okna dialogowego jest to wbudowane działanie domyślne:

Jeśli chcesz animować okno, utracisz tę funkcję. W sekcji JavaScript przywrócim tę funkcję.

Trafna koncentracja

Element dialogowy zarządza za Ciebie inert w dokumencie. Przed inert do wykrywania, czy element został przechwycony i umieszczany z powrotem, używano JavaScriptu.

Obsługa przeglądarek

  • 102
  • 102
  • 112
  • 15.5

Źródło

Po inert dowolne części dokumentu mogą zostać „zablokowane” na tyle, że nie są już celem lub są interaktywne przy użyciu myszy. Zamiast skupiać się na pułapce, następuje skupienie się na jedynej interaktywnej części dokumentu.

Otwieranie elementu i automatyczne ustawianie ostrości

Domyślnie element okna będzie przypisywał zaznaczenie do pierwszego elementu, który można zaznaczyć w znacznikach okna. Jeśli nie jest to najlepszy element dla użytkownika, użyj atrybutu autofocus. Jak pisaliśmy wcześniej, uważam, że najlepiej jest umieścić go na przycisku anulowania, a nie na przycisku potwierdzenia. Dzięki temu masz pewność, że potwierdzenie jest celowe, a nie przypadkowe.

Zamykanie za pomocą klawisza Escape

Ważne jest, aby użytkownik mógł łatwo zamknąć ten potencjalnie zakłócający element. Na szczęście element dialogowy będzie obsługiwać klawisz Escape, dzięki czemu uwolnisz się od pracy związanej z administracją.

Style

Istnieje prosta ścieżka do stylizacji elementu okna i trudna ścieżka. Najłatwiej jest to osiągnąć, nie zmieniając właściwości wyświetlania okna i uwzględniając jego ograniczenia. Wykonuję trudną ścieżkę, aby uzyskać niestandardowe animacje do otwierania i zamykania okna, przejmując właściwość display i inne elementy.

Stylizacja za pomocą otwartych rekwizytów

Aby przyspieszyć adaptacyjne kolory i ogólną spójność projektu, bez wstydu dodałem do biblioteki zmiennych CSS narzędzia Open Props. Oprócz podanych bezpłatnie zmiennych importuję też plik normalizuj i niektóre przyciski, które umożliwiają importowanie opcjonalnych narzędzi Open Props. Pomagają mi one skupić się na dostosowywaniu okna i wersji demonstracyjnej, nie wymagając przy tym wielu stylów.

Stylizowanie elementu <dialog>

Własność usługi displayowej

Domyślny sposób pokazywania i ukrywania elementu okna przełącza właściwość wyświetlania z block na none. Oznacza to, że nie można go animować od początku i na zewnątrz, tylko w środku. Chcę używać animacji zarówno w reklamie, jak i na zewnątrz. Pierwszym krokiem jest ustawienie własnej usługi display:

dialog {
  display: grid;
}

Zmieniając wartość właściwości wyświetlania (i tym samym nadając jej własność), jak widać powyżej we fragmencie kodu CSS, trzeba zarządzać sporą liczbą stylów, aby zapewnić użytkownikom odpowiedni komfort. Po pierwsze okno domyślne jest zamknięte. Możesz zaprezentować ten stan wizualnie i uniemożliwić okno dialogowe odbieranie interakcji z tymi stylami:

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

Teraz okno jest niewidoczne i nie można go wchodzić w interakcję, gdy nie jest otwarte. Później dodam kod JavaScript, aby zarządzać atrybutem inert w oknie, tak aby użytkownicy klawiatury i czytników ekranu również nie mogli przejść do ukrytego okna.

Nadanie dialogowi motywu kolorystycznego

Megaokno z jasnym i ciemnym motywem, w którym widać kolory powierzchni.

Chociaż narzędzie color-scheme włącza w dokumencie dostępny w przeglądarce adaptacyjny motyw kolorów do wyświetlania w ustawieniach systemu jasnego i ciemnego, chciałem bardziej dostosować element okna. Open Props udostępnia kilka kolorów powierzchni, które automatycznie dostosowują się do ustawień jasnego i ciemnego systemu, podobnie jak w przypadku color-scheme. Świetnie sprawdzają się przy tworzeniu warstw w projekcie. Uwielbiam używać kolorów, aby wizualnie oddać taki wygląd powierzchni. Kolor tła to var(--surface-1). Aby umieścić je na górze tej warstwy, użyj kodu 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 do elementów podrzędnych, takich jak nagłówek i stopka, zostaną dodane bardziej elastyczne kolory. Uważam, że to dodatkowy element dialogów, ale są naprawdę ważne, jeśli chodzi o atrakcyjność i dobrze zaprojektowaną strukturę okna.

Rozmiar elastycznych okien

Domyślnym ustawieniem w oknie jest delegowanie rozmiaru jego zawartości, co jest ogólnie korzystne. Moim celem jest ograniczenie obszaru max-inline-size do czytelnego rozmiaru (--size-content-3 = 60ch) lub 90% szerokości widocznego obszaru. Dzięki temu okno nie będzie rozciągać się od krawędzi do krawędzi na urządzeniu mobilnym ani nie będzie tak szerokie, że trudno będzie odczytać tekst na ekranie komputera. Następnie dodaję max-block-size, aby okno nie przekraczało wysokości strony. Musimy też określić, gdzie znajduje się przewijany obszar okna, na wypadek gdyby okno było wysokie.

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

Widzisz, że pojawia się 2 razy: max-block-size? Pierwszy to 80vh – fizyczna jednostka widocznego obszaru. Zależy mi na tym, aby okno dialogowe dla użytkowników międzynarodowych znajdowało się w prędkości względnej, więc w drugiej deklaracji używam logicznej, nowszej i tylko częściowo obsługiwanej jednostki dvb, gdy stanie się ona bardziej stabilna.

Pozycjonowanie megaokna dialogowego

Aby pomóc w pozycjonowaniu elementu okna, warto podzielić jego 2 części: pełnoekranowe tło i kontener okna. Tło musi zakrywać wszystko, co da efekt cieniowania, który pomaga podkreślić, że to okno znajduje się na pierwszym planie, a zawartość z tyłu jest niedostępna. Kontener okna może się wyśrodkować na tym tle i przybrać dowolny kształt, którego wymaga jego zawartość.

Poniższe style powodują trwałe dopasowanie elementu okna do okna, rozciąganie go do każdego rogu i używanie elementu margin: auto do wyśrodkowania treści:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Style megaokna na komórce

Na małych widocznych obszarach mój megamodal na całej stronie wygląda trochę inaczej. Ustawiam dolny margines na 0, dzięki czemu treść okna znajduje się na dole widocznego obszaru. Dzięki kilku modyfikacjom stylu mogę zmienić okno w arkusz działań 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;
  }
}

Zrzut ekranu z narzędziami deweloperskimi, które nakładają odstępy między marginesami w dużym oknie na komputerze i urządzeniu mobilnym (po otwarciu).

Pozycjonowanie miniokna

Gdy korzystam z większego widocznego obszaru, np. na komputerze, staram się umieścić miniokna dialogowe nad elementem, który je wywołał. Potrzebuję do tego JavaScriptu. Mogą Państwo zapoznać się z metodą, której używam, ale moim zdaniem nie wykracza ona poza zakres tego artykułu. Bez JavaScriptu miniokno dialogowe wyświetla się na środku ekranu, jak megaokno dialogowe.

Wyróżnij się

Na koniec dodaj kilka urozmaiceń do okna, aby wyglądały jak miękka powierzchnia leżąca daleko nad stroną. Miękkość osiąga się przez zaokrąglenie rogów okna. Głębia ta jest za pomocą jednej ze starannie zaprojektowanych rekwizytów cieniowych:

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

Dostosowywanie pseudoelementu tła

Zdecydowałem się pracować nad tłem bardzo delikatnie i dodałam efekt rozmycia w backdrop-filter do dużego okna:

Obsługa przeglądarek

  • 76
  • 17
  • 103
  • 9

Źródło

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

Dodaję też przejście backdrop-filter, mam nadzieję, że w przyszłości przeglądarki pozwolą na przenoszenie elementu tła:

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

Zrzut ekranu przedstawiający duże okno z nałożonym rozmytym tłem kolorowych awatarów.

Dodatki do stylu

Określam tę sekcję jako „extras”, ponieważ ma ona więcej wspólnego z demonstracją elementu okna dialogowego niż z elementem dialogu w ogóle.

Schowek

Po wyświetleniu okna użytkownik nadal może przewijać stronę, która jest za nią, a nie chcę:

Zwykle najlepiej użyć narzędzia overscroll-behavior, ale zgodnie ze specyfikacją nie ma to wpływu na okno, ponieważ nie jest to port przewijania, czyli nie jest przewijany, więc nie ma nic przeciwko temu. Można użyć JavaScriptu, aby wykryć nowe zdarzenia z tego przewodnika, takie jak „closed” (zamknięte) i „opened” (otwarte), a następnie przełączyć overflow: hidden w dokumencie, lub poczekać, aż :has() będzie działać stabilnie we wszystkich przeglądarkach:

Obsługa przeglądarek

  • 105
  • 105
  • 121
  • 15,4

Źródło

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

Teraz po otwarciu dużego okna, dokument HTML zawiera plik overflow: hidden.

Układ <form>

To nie tylko bardzo ważny element zbierania informacji o interakcji od użytkownika, ale też elementów nagłówka, stopki i artykułu. Zamierzam przedstawić element podrzędny artykułu jako obszar, który można przewijać. Osiągam to w grid-template-rows. Element artykułu jest nazywany 1fr, a formularz ma taką samą maksymalną wysokość co element okna. Dzięki ustawieniu tej stałej wysokości i twardego rozmiaru wiersza można zablokować element artykułu i przewinąć, gdy się wyczerpie:

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

Zrzut ekranu z narzędziami deweloperskimi nakładającymi się na informacje o układzie siatki nad wierszami.

Styl okna <header>

Jego rola to podanie tytułu zawartości okna i udostępnienie łatwego do znalezienia przycisku zamykania. Artykuł ma też kolor powierzchni, przeznaczony za treść okna. Te wymagania wiążą się z kontenerem Flexbox, pionowo wyrównanymi elementami rozmieszczonymi od krawędzi oraz pewnymi dopełnieniem i lukami, które zapewniają miejsce dla przycisków tytułu i zamykania:

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

Zrzut ekranu przedstawiający Narzędzia deweloperskie w Chrome z nałożonymi informacjami o układzie Flexbox w nagłówku okna.

Styl przycisku zamykania nagłówka

Ponieważ w wersji demonstracyjnej znajdują się przyciski Otwórz rekwizyty, przycisk zamykania jest przystosowany do okrągłych ikon. Przykład:

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;
}

Zrzut ekranu z narzędziami deweloperskimi Chrome, które nakładają informacje o rozmiarze i dopełnieniu przycisku zamykania nagłówka.

Styl okna <article>

Element artykułu ma w tym oknie specjalną rolę: jest to obszar, który należy przewinąć w przypadku wysokiego lub długiego okna.

Aby to osiągnąć, nadrzędny element formularza sam wyznaczył pewne wartości maksymalne, które ograniczają zasięg elementu artykułu, gdy jest za wysoki. Ustaw element overflow-y: auto tak, aby paski przewijania wyświetlały się tylko wtedy, gdy są potrzebne, i zawierać przewijanie w obrębie elementu overscroll-behavior: contain, a pozostałe style prezentacji były niestandardowe:

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

Rola stopki obejmuje menu z przyciskami poleceń. Flexbox służy do wyrównania treści do końca osi wbudowanej stopki, a następnie zostawienie trochę miejsca na przyciskach.

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

Zrzut ekranu z Narzędziami deweloperskimi Chrome, które nakładają informacje o układzie Flexbox na element stopki.

Element menu zawiera przyciski poleceń okna. Wykorzystuje układ zawijania Flexbox z elementem gap, który zapewnia odstęp między przyciskami. Elementy menu mają dopełnienie, np. <ul>. Usuwam też ten styl, bo jest mi niepotrzebny.

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;
}

Zrzut ekranu z Narzędziami deweloperskimi Chrome, które nakładają informacje Flexbox na elementy menu stopki.

Animacja

Elementy okien są często animowane, ponieważ otwierają się i zamykają okno. Wzmocnienie dialogów w kontekście wejścia i wyjścia pozwala użytkownikom zorientować się w procesie.

Normalnie element okna może być animowany tylko w środku, ale nie na zewnątrz. Dzieje się tak, ponieważ przeglądarka przełącza właściwość display w elemencie. Wcześniej przewodnik ustawił wyświetlanie na siatce, ale nigdy nie ustawiał żadnej. To daje możliwość animowania od wewnątrz i na zewnątrz.

Open Props udostępnia wiele animacji klatki kluczowej, dzięki którym aranżacja jest łatwa i czytelna. Oto moje cele animacji i warstwy:

  1. Domyślnym przejściem jest ograniczony ruch – proste rozjaśnianie i zmniejszanie przezroczystości.
  2. Jeśli ruch jest prawidłowy, zostaną dodane animacje przesuwania i skalowania.
  3. Elastyczny układ mobilny dużego okna jest dostosowywany do wysuwania się.

Bezpieczne i wymierne przejście na nową wersję.

Chociaż Open Props obejmuje klatki kluczowe, które pozwalają rozjaśniać i znikać, wolę to warstwowe podejście jako domyślne, a w razie potencjalnych ulepszeń wolę używać animacji klatek kluczowych. Wcześniej nadaliśmy już styl widoczności okna przezroczystość, administrując 1 lub 0 w zależności od atrybutu [open]. Aby przejść między 0% a 100%, powiedz przeglądarce, jak długo i jakiego rodzaju wygładzanie chcesz zastosować:

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

Dodaje ruch do przejścia

Jeśli użytkownik nie przeszkadza w ruchu, zarówno duże okno, jak i miniokno dialogowe, powinny się przesunąć w górę jako wejście i skalować się wraz z wyjściem. Aby to zrobić, użyj zapytania o multimedia prefers-reduced-motion i kilku 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;
  }
}

Dostosowanie animacji wyjściowej do urządzenia mobilnego

Na wcześniejszym etapie w sekcji dotyczącej stylizacji styl megaokna jest przystosowany do urządzeń mobilnych, aby przypominał arkusz działań – jakby kartka papieru wysunęła się z dołu ekranu i nadal była na dole. Animacja wyjściowa skalowania w poziomie nie pasuje do nowego układu i możemy ją dostosować za pomocą kilku zapytań o multimedia i kilku 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

W języku JavaScript trzeba dodać kilka rzeczy:

// 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 zmiany wynikają z chęci szybkiego zamknięcia okna (kliknięcia tła okna), animacji i kilku dodatkowych zdarzeń, które pozwalają szybciej pobrać dane formularza.

Wyłącz diodę

To zadanie jest proste i stanowi świetny dodatek do nieanimowanego elementu dialogowego. Interakcję można uzyskać, obserwując kliknięcia elementu okna i wykorzystując dymki zdarzeń do oceny tego, co zostało kliknięte. close() będzie stosowane tylko wtedy, gdy jest to element najwyższego poziomu:

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

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

Uwaga: dialog.close('dismiss'). Zdarzenie zostanie wywołane i podany ciąg znaków. Ten ciąg może zostać pobrany przez inny JavaScript, by uzyskać wgląd w sposób zamknięcia okna. Przy każdym wywołaniu funkcji z poziomu różnych przycisków umieszczam też ciągi znaków zamykające, aby zapewnić mojej aplikacji kontekst dotyczący interakcji z użytkownikiem.

Dodawanie zdarzeń zamknięcia i zamknięcia

Element okna ma zdarzenie zamknięcia, które jest wysyłane natychmiast po wywołaniu funkcji okna close(). Ponieważ animujemy ten element, warto mieć zdarzenia przed i po animacji, aby zmienić dane lub zresetować okno dialogowe. Używam go tutaj do zarządzania dodawaniem atrybutu inert w zamkniętym oknie, a w wersji demonstracyjnej używam ich do modyfikowania listy awatarów, gdy użytkownik prześle nowe zdjęcie.

W tym celu utwórz 2 nowe zdarzenia o nazwie closing i closed. Następnie nasłuchuj wbudowanego zdarzenia zamknięcia w oknie. W tym miejscu ustaw okno na inert i wyślij zdarzenie closing. Następnym zadaniem jest poczekać na zakończenie działania animacji i przejść w oknie, a potem wysłać zdarzenie 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 również używana w przypadku tworzenia komponentu toastu, zwraca obietnicę zależną od ukończenia animacji i obietnicy przejścia. Właśnie dlatego dialogClose jest funkcją asynchroniczną. Dzięki temu może await zwrócić obietnicę i z pewnością przejść do zdarzenia zamkniętego.

Dodawanie zdarzeń otwarcia i otwarcia

Nie jest łatwo dodać te zdarzenia, ponieważ wbudowany element okna nie zapewnia zdarzenia otwarcia, jak ma to miejsce w przypadku zamknięcia. Używam narzędzia MutationObserver, aby udostępniać informacje o zmieniających się atrybutach okna. Będę obserwować zmiany w atrybucie open source i odpowiednio zarządzać zdarzeniami niestandardowymi.

Podobnie jak w przypadku wydarzeń zamknięcia i zamknięcia utwórz 2 nowe zdarzenia o nazwie opening i opened. Do tej pory wychwytywaliśmy zdarzenie zamknięcia w oknie, tym razem jednak do obserwowania atrybutów okna użyj utworzonego obserwatora mutacji.

…
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 mutacji jest wywoływana po zmianie atrybutów okna, zapewniając listę zmian w postaci tablicy. Przeanalizuj zmiany atrybutów i sprawdź, czy attributeName jest otwarty. Następnie sprawdź, czy element ma atrybut. Dzięki temu dowiesz się, czy okno zostało otwarte. Jeśli został otwarty, usuń atrybut inert i ustaw zaznaczenie na element żądający autofocus lub pierwszy element button znaleziony w oknie. Podobnie jak w przypadku zdarzenia zamknięcia i zamknięcia, od razu wysyłaj zdarzenie otwierające, poczekaj na zakończenie animacji, a następnie wyślij otwarte zdarzenie.

Dodawanie usuniętego wydarzenia

W aplikacjach jednostronicowych okna są często dodawane i usuwane w zależności od tras lub innych potrzeb i stanu aplikacji. Może to być przydatne, jeśli chcesz wyczyścić zdarzenia lub dane po usunięciu okna.

Możesz to osiągnąć, korzystając z innego obserwatora mutacji. Tym razem zamiast obserwować atrybuty elementu okna, będziemy obserwować elementy podrzędne elementu nadrzędnego i sprawdzać, czy nie są usuwane elementy dialogowe.

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

Wywołanie zwrotne obserwatora mutacji jest wywoływane za każdym razem, gdy użytkownik dodaje elementy podrzędne do dokumentu lub je z niego usuwa. Konkretne obserwowane mutacje dotyczą parametru removedNodes, który ma nodeName okna dialogowego. Jeśli okno zostało usunięte, zdarzenia kliknięcia i zamknięcia są usuwane, aby zwolnić pamięć, a usunięte niestandardowe zdarzenie jest wysyłane.

Usuwanie atrybutu wczytywania

Aby uniemożliwić odtwarzanie animacji wyjścia po dodaniu do strony lub jej wczytywania, do okna dialogowego dodano atrybut wczytywania. Ten skrypt czeka na zakończenie działania animacji w oknie, a potem usuwa atrybut. Teraz okno można animować na zewnątrz i na zewnątrz. Skutecznie ukryliśmy animację rozpraszającą uwagę.

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

Więcej informacji o problemie zapobiegania animowaniu klatek kluczowych podczas wczytywania strony znajdziesz tutaj.

Wszystko razem

Oto dialog.js z całością, a potem omówiliśmy każdą sekcję z osobna:

// 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, że zostanie wywołana i przekazana elementowi okna, w którym chcesz dodać te nowe zdarzenia i funkcje:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Tak jak w przypadku 2 okien, wprowadziliśmy możliwość lekkiego zamknięcia okien, poprawki ładowania animacji i dodatkowe zdarzenia, z którymi można pracować.

Nasłuchiwanie nowych zdarzeń niestandardowych

Każdy uaktualniony element okna może teraz nasłuchiwać 5 nowych zdarzeń, na przykład:

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 wersji demonstracyjnej utworzonej za pomocą elementu okna używam zdarzenia zamknięcia i danych formularza, aby dodać do listy nowy element awatara. Czas jest dobry, gdy okno dialogowe kończy się animacja zamknięcia, a potem w nowym awatarze pojawiają się skrypty animacji. Dzięki nowym zdarzeniom organizowanie wrażeń użytkownika może być łatwiejsze.

Uwaga dialog.returnValue: zawiera ciąg znaków zamykający przekazywany po wywołaniu zdarzenia close() w oknie dialogowym. W zdarzeniu dialogClosed jest niezbędne, żeby wiedzieć, czy okno zostało zamknięte, anulowane czy potwierdzone. Jeśli zostanie on potwierdzony, skrypt pobierze wartości z formularza i go resetuje. Jest to przydatne, ponieważ po ponownym wyświetleniu okna okno jest puste i gotowe do przesłania nowego zgłoszenia.

Podsumowanie

Wiesz już, jak to zrobiłem, więc jak to zrobisz 🙂

Stwórzmy różne metody i nauczmy się wszystkiego, jak rozwijać się w internecie.

Utwórz demonstrację i udostępnię mi linki na Twitterze, a dodam ją do sekcji remiksów w ramach społeczności poniżej.

Remiksy społeczności

Zasoby