Tworzenie komponentu okna

Podstawowy przegląd tworzenia dostosowanych do koloru, responsywnych i dostępnych mini- i megamodali za pomocą elementu <dialog>.

W tym poście chcę podzielić się z Wami swoimi przemyśleniami na temat tworzenia dostosowywających się do kolorów, responsywnych i dostępnych mini-modali oraz mega-modali za pomocą elementu <dialog>. Wypróbuj wersję demonstracyjną i zobacz źródło.

Demonstracja mega i mini dialogów w jasnym i ciemnym motywie.

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

Omówienie

Element <dialog> świetnie nadaje się do umieszczania na stronie informacji kontekstowych lub działań. Zastanów się, kiedy użytkownik może skorzystać z działania na tej samej stronie zamiast działania na wielu stronach. Może to być spowodowane tym, że formularz jest krótki lub użytkownik musi tylko potwierdzić lub anulować.

Element <dialog> jest ostatnio stabilny w różnych przeglądarkach:

Obsługa przeglądarek

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Źródło

Zauważyłem, że w elemencie brakuje kilku rzeczy, dlatego w tym wyzwaniu GUI dodam elementy interfejsu dla programistów: dodatkowe zdarzenia, lekkie zamknięcie, niestandardowe animacje oraz miniaturkę i megatyp.

Znacznik

Element <dialog> jest prosty. Element zostanie automatycznie ukryty i będzie miał wbudowane style, które nałożą się na Twoje treści.

<dialog>
  …
</dialog>

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

Element dialogu ma wiele wspólnego z elementem modalnym i często te nazwy są używane zamiennie. Użyłem elementu okna dialogowego zarówno w przypadku małych okien (mini), jak i okien na całą stronę (mega). Nazwałem je mega i mini. Oba dialogi są nieco dostosowane do różnych przypadków użycia. 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 dialogami mini i mega w wersji jasnej i ciemnej.

Nie zawsze, ale zazwyczaj elementy dialogu służą do zbierania pewnych informacji o interakcjach. Formularze w elementach dialogów ze sobą współpracują. Warto umieścić treść dialogu w elemencie formularza, aby kod JavaScript miał dostęp do danych wprowadzonych przez użytkownika. Ponadto przyciski w formularzu, które używają elementu method="dialog", mogą zamykać okno bez JavaScriptu i przekazywania danych.

<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: <header>, <article> i <footer>. Stanowią one kontenery semantyczne, a także cele stylu dla prezentacji dialogu. Nagłówek zawiera tytuł i zawiera przycisk zamykania. Ten artykuł dotyczy danych i informacji wprowadzanych w formularzu. Stopka zawiera <menu> przyciski 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 maautofocusi wbudowany w kodu sposób obsługi zdarzenia onclick. Atrybut autofocus będzie aktywny, gdy otworzy się okno. Najlepiej umieścić go przy przycisku anulowania, a nie przycisku potwierdzenia. Dzięki temu potwierdzenie jest celowe, a nie przypadkowe.

Miniokno

Mini okno dialogowe jest bardzo podobne do mega okna dialogowego, ale nie zawiera elementu <header>. Dzięki temu będą mniejsze i lepiej wbudowane.

<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 pełnego elementu widoku, który może zbierać dane i reagować na interakcje użytkownika. Te podstawowe elementy mogą zapewnić bardzo interesujące i skuteczne interakcje w Twojej witrynie lub aplikacji.

Ułatwienia dostępu

Element dialogu ma bardzo dobre wbudowane ułatwienia dostępu. Zamiast dodawać te funkcje, jak zwykle, wiele z nich jest już dostępnych.

Przywracanie ostrości

Podobnie jak w komponencie menu bocznego, tak i w tym przypadku ważne jest, aby otwieranie i zamykanie było prawidłowe i aby skupiało uwagę na odpowiednich przyciskach otwierania i zamykania. Po otwarciu panelu bocznego zaznaczenie zostanie umieszczone na przycisku zamykania. Po naciśnięciu przycisku Zamknij fokus wraca do przycisku, który został otwarty.

W przypadku elementu dialogu jest to wbudowane domyślne zachowanie:

Jeśli chcesz animować dialog, ta funkcja nie będzie działać. Przywrócę tę funkcję w sekcji JavaScript.

Zablokowanie ostrości

Element dialogu zarządzainertw dokumencie. Przed inert do sprawdzania, czy fokus opuszcza element, używano JavaScriptu, który przechwytywał fokus i zwracał go.

Obsługa przeglądarek

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Źródło

Po inert dowolne części dokumentu mogą zostać zablokowane tak, że nie będą już zaznaczać elementów ani nie będą interaktywne po naciśnięciu myszy. Zamiast blokowania punktu skupienia, punkt skupienia jest kierowany na jedyną interaktywną część dokumentu.

Otwieranie elementu i automatyczne ustawianie ostrości

Domyślnie element okna aktywuje pierwszy element, który można zaznaczyć, w znacznikach okna. Jeśli nie jest to najlepszy element domyślny dla użytkownika, użyj atrybutu autofocus. Jak już wspomniałem, według mnie sprawdzoną metodą jest umieszczanie tego przycisku przy przycisku anulowania, a nie przycisku potwierdzenia. Dzięki temu możesz mieć pewność, że potwierdzenie jest celowe, a nie przypadkowe.

Zamykanie klawisza Escape

Ważne, aby można było łatwo zamknąć ten potencjalnie uciążliwy element. Na szczęście element dialogu obsłuży za Ciebie klawisz Esc, co odciąży Cię od konieczności koordynowania.

Style

Istnieje prosty sposób na określenie stylu elementu dialogowego i sztywnej ścieżki. Łatwy sposób polega na nie zmienianiu właściwości wyświetlania okna dialogowego i pracy z jego ograniczeniami. Aby uzyskać niestandardowe animacje otwierania i zamykania okna dialogowego oraz przejęcia właściwości display i innych, wybieram skomplikowaną ścieżkę.

Stylizacja za pomocą otwartych obiektów

Aby przyspieszyć dostosowywanie kolorów i ujednolicenie projektu, bezwstydliwie użyłam mojej biblioteki zmiennych CSS Open Props. Oprócz bezpłatnych zmiennych importuję też plik normalize i kilka przycisków, które Open Props udostępniają jako opcjonalne importy. Dzięki importowaniu mogę skupić się na dostosowywaniu dialogu i demonstracji, nie wymagając przy tym wielu stylów, aby uzyskać dobry wygląd.

Stylizacja elementu <dialog>

Posiadanie usługi displayowej

Domyślne działanie opcji pokazywania i ukrywania elementu okna przełącza właściwość wyświetlania z block na none. Niestety nie można ich animować od początku i na zewnątrz, tylko do wewnątrz. Chcę animować zarówno wejście, jak i wyjście, a pierwszym krokiem jest ustawienie własnej właściwości wyświetlania:

dialog {
  display: grid;
}

Zmiana wartości właściwości wyświetlania, a więc przejęcie nad nią kontroli, jak pokazano w powyższym fragmencie kodu CSS, wymaga zarządzania znaczną liczbą stylów, aby zapewnić użytkownikom odpowiednie wrażenia. Po pierwsze, domyślny stan dialogu to „zamknięty”. Możesz wizualnie reprezentować ten stan i uniemożliwić dialogowi otrzymywanie interakcji za pomocą tych stylów:

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

Teraz okno jest niewidoczne i nie można z nim korzystać, gdy nie jest otwarte. Później dodam kod JavaScript, który będzie zarządzać atrybutem inert w dialogu, aby użytkownicy korzystający z klawiatury i czytnika ekranu nie mogli uzyskać dostępu do ukrytego dialogu.

nadać oknu kolory za pomocą motywu kolorystycznego,

Okno dialogowe pokazujące motyw jasny i ciemny oraz kolory powierzchni.

Chociaż color-scheme przełącza dokument na schemat kolorów dostosowujący się do jasnego i ciemnego motywu systemu w przeglądarce, chciałem dostosować element okna w bardziej szczegółowy sposób. Open Props zawiera kilka kolorów powierzchni, które automatycznie dostosowują się do jasnych i ciemnych ustawień systemu, podobnie jak w przypadku color-scheme. Te opcje są świetne do tworzenia warstw w projektach. Lubię używać kolorów, aby wizualnie wspierać wygląd powierzchni warstw. Kolor tła to var(--surface-1); aby umieścić obiekt 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);
  }
}

W przyszłości dodamy więcej kolorów dostosowujących się do tła w przypadku elementów podrzędnych, takich jak nagłówek i stopka. Uważam, że są one dodatkowym elementem dialogu, ale bardzo ważnym w tworzeniu atrakcyjnego i dobrze zaprojektowanego dialogu.

Elastyczne dopasowywanie rozmiaru dialogu

Domyślnie rozmiar okna jest dobierany do zawartości, co jest 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 dialogowe nie będzie zajmować całego ekranu na urządzeniu mobilnym i nie będzie na tyle szerokie na ekranie komputera, że będzie trudno je odczytać. Następnie dodaję max-block-size, aby okno nie przekraczało wysokości strony. Musisz też określić, gdzie znajduje się przewijany obszar okna, jeśli jest to wysoki element.

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

Zauważyłaś, że max-block-size jest podwójnie? Pierwszy z nich używa 80vh, fizycznej jednostki viewport. Bardzo zależy mi na zachowaniu dialogu w ramach względnego procesu w przypadku użytkowników z innych krajów, dlatego w drugiej deklaracji używam logicznej, nowszej i tylko częściowo obsługiwanej jednostki dvb, gdy stanie się ona bardziej stabilna.

Pozycjonowanie okna dialogowego Mega

Aby ułatwić określenie odpowiedniego położenia elementu okna, warto podzielić go na 2 części: tło pełnoekranowe i kontener okna. Tło musi obejmować wszystko i zapewnia efekt cieniowania, który podkreśla, że okno dialogowe znajduje się na pierwszym planie, a treść nie jest dostępna. Kontener dialogu może swobodnie wyśrodkować się na tle i przybrać dowolny kształt, jaki wymagają jego zawartości.

Te style przypinają element okna dialogowego do okna, rozciągając go do każdego rogu, i używają margin: auto, aby wyśrodkować zawartość:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Style mega okienek na urządzeniach mobilnych

W przypadku małych widoków stylizuję ten megamodal na całą stronę nieco inaczej. Ustawiłem dolny margines na 0, co powoduje, że treść dialogu jest wyświetlana na dole widocznego obszaru. Po kilku zmianach stylu mogę przekształcić okno dialogowe w panel czynności, który jest bliższy kciukom 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 pokazujący nakładkę narzędzi dewelopera na marginesach w ramach otwartego okna dialogowego na komputerze i na urządzeniu mobilnym

Pozycjonowanie minidialogu

Gdy używasz większego widoku, np. na komputerze stacjonarnym, minidialogi możesz umieścić nad elementem, który je wywołał. Do tego potrzebuję JavaScriptu. Technikę, której używam, znajdziesz tutaj, ale uważam, że wykracza ona poza zakres tego artykułu. Bez kodu JavaScript mini okno dialogowe pojawi się pośrodku ekranu, tak jak mega okno.

Wyróżnij się

Na koniec dodaj do dialogu nieco uroku, aby wyglądał jak miękka powierzchnia znajdująca się wysoko nad stroną. Miękkość uzyskuje się przez zaokrąglenie rogów okna. Głębia jest osiągana dzięki jednemu z ostrożnie wykonanych elementów cieni Open Props:

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

Dostosowywanie pseudoelementu tła

Tło nie zostało zbytnio zmienione, dodano tylko efekt rozmyciabackdrop-filter do okna dialogowego:

Obsługa przeglądarek

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Źródło

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

Zamierzam też ustawić przejście w elemencie backdrop-filter, mając nadzieję, że w przyszłości przeglądarki pozwolą na zmianę elementu tła:

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

Zrzut ekranu z oknem mega nałożonym na zamazane tło z kolorowymi awatarami.

Dodatki do stylizacji

Nazywam tę sekcję „Dodatki”, ponieważ ma ona więcej wspólnego z elementem dialogu w moim demo niż z elementem dialogu w ogóle.

Ograniczenia dotyczące przewijania

Gdy wyświetla się okno dialogowe, użytkownik nadal może przewijać stronę, co nie jest pożądane:

Zwykle overscroll-behavior byłoby moim ulubionym rozwiązaniem, ale zgodnie ze specyfikacją nie ma ono wpływu na okno dialogowe, ponieważ nie jest to element sterujący przewijaniem, a więc nie ma nic do zapobiegania. Mogę użyć JavaScriptu, aby obserwować nowe zdarzenia z tego przewodnika, takie jak „zamknięte” i „otwarte”, oraz włączać i wyłączać overflow: hidden w dokumentach. Mogę też poczekać, aż :has() będzie stabilny we wszystkich przeglądarkach:

Obsługa przeglądarek

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Źródło

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

Teraz po otwarciu megaokna dokument HTML zawiera overflow: hidden.

Układ <form>

Poza tym, że jest to bardzo ważny element do zbierania informacji o interakcji użytkownika, wykorzystuję go tutaj do rozmieszczania elementów nagłówka, stopki i artykułu. W tym układzie artykuł podrzędny ma być przedstawiony jako obszar do przewijania. Używam do tego grid-template-rows. Element artykułu ma wartość 1fr, a samo okno ma tę samą maksymalną wysokość co element dialogu. Ustawienie tej stałej wysokości i stałego rozmiaru wiersza pozwala ograniczyć element artykułu i przewijać go, gdy się rozszerzy:

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

Zrzut ekranu przedstawiający narzędzia deweloperskie nakładające informacje o układzie siatki na wiersze.

Stylizacja okna dialogowego <header>

Rolą tego elementu jest nadanie tytułu zawartości okna i udostępnienie łatwego do znalezienia przycisku zamykania. Ma ona też kolor powierzchni, dzięki czemu wydaje się sąsiadować z treścią artykułu. Te wymagania prowadzą do użycia kontenera flexbox, elementów wyrównanych pionowo, rozmieszczonych w odstępach od krawędzi oraz odstępów i przestrzeni między tytułem a przyciskami 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.

Nadawanie stylu przyciskowi zamykania nagłówka

Ponieważ w demonstracji używane są przyciski Otwarte rekwizyty, przycisk zamykania zmienia się w okrągły, skupiony wokół ikony przycisk, jak w 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;
}

Zrzut ekranu pokazujący nakładkę z informacjami o rozmiarach i wypełnieniu przycisku zamykania nagłówka w Narzędziach deweloperskich Chrome

Określanie stylu okna dialogowego <article>

Element artykułu pełni w tym dialogu szczególną rolę: jest to miejsce przeznaczone do przewijania w przypadku długiego lub wysokiego dialogu.

Aby to osiągnąć, element formularza nadrzędnego ma określone maksymalne wartości, które narzucają ograniczenia dla tego elementu artykułu, gdy stanie się on zbyt wysoki. Ustaw overflow-y: auto tak, aby paski przewijania wyświetlały się tylko wtedy, gdy są potrzebne, zawierały przewijanie w ramach overscroll-behavior: contain, a reszta była wyświetlana w niestandardowym stylu:

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

Stopka zawiera menu z przyciskami czynności. Flexbox służy do wyrównywania treści do końca osi w stopce, a następnie dodawania odstępów, aby zapewnić wystarczającą ilość miejsca dla przycisków.

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 przedstawiający nakładanie się informacji o układzie flexbox na elemencie stopki w Narzędziach dla programistów w Chrome

Element menu służy do umieszczania przycisków akcji w dialogu. Używa ona układu flexbox z elementem gap, aby zapewnić odstęp między przyciskami. Elementy menu mają wypeł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;
}

Zrzut ekranu przedstawiający narzędzia deweloperskie w Chrome z nałożonymi informacjami dotyczącymi Flexbox na elementy menu stopki.

Animacja

Elementy dialogu są często animowane, ponieważ pojawiają się i znikają z okna. Dodanie animacji podczas wchodzenia i wychodzenia postaci pomaga użytkownikom w orientowaniu się w fabułe.

Zazwyczaj element dialogu może być animowany tylko przy wstawianiu, a nie przy usuwaniu. Dzieje się tak, ponieważ przeglądarka przełącza w elemencie właściwość display. Wcześniej przewodnik ustawiał wyświetlanie na siatkę, ale nigdy nie ustawiał jej na brak. Dzięki temu możesz animować otwieranie i zamykanie.

Open Props zawiera wiele animacji keyframe, które ułatwiają i ułatwiają czytanie. Oto cele animacji i warstwowy sposób ich realizacji:

  1. Mniej animacji to domyślne przejście, czyli proste zniknięcie i ponowne pojawienie się obrazu z zmniejszającą się przezroczystością.
  2. Jeśli ruch jest prawidłowy, dodawane są animacje przesuwania i skalowania.
  3. Responsywny układ mobilny megaokna jest dostosowany do przesuwania.

bezpieczne i wartościowe przeniesienie domyślne;

Open Props zawiera klatki kluczowe do stosowania efektu płynnego przejścia, ale wolę stosować domyślnie ten warstwowy sposób tworzenia przejść z animacjami klatek kluczowych jako potencjalnym ulepszeniem. Wcześniej stylizowaliśmy widoczność dialogu za pomocą przezroczystości, sterując 1 lub 0 w zależności od atrybutu [open]. Aby przejść między 0% a 100%, poinformuj przeglądarkę, na jak długo i jaki ma być rodzaj wygładzania:

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

Dodawanie ruchu do przejścia

Jeśli użytkownik zgadza się na ruch, zarówno okno dialogowe mega, jak i mini powinno się przesuwać w górę podczas pojawiania się i wysuwać w dół podczas znikania. Możesz to osiągnąć za pomocą zapytania o media prefers-reduced-motion i kilku otwartych rekwizytów:

@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 na potrzeby urządzeń mobilnych

W sekcji dotyczącej stylów omówiliśmy styl megadialogu dostosowany do urządzeń mobilnych, który przypomina kartę działań. Wygląda tak, jakby mały kawałek papieru przesunął się w górę od dołu ekranu i nadal jest do niego przymocowany. Animacja wyjścia z powiększeniem nie pasuje do nowego projektu. Możemy ją dostosować za pomocą kilku zapytań o multimedia i otwartych komponentów:

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

Wynika to z potrzeby lekkiego zamknięcia okna (kliknięcia tła okna), animacji i kilku dodatkowych zdarzeń, które poprawiają czas pobierania danych formularza.

Dodawanie Dismiss

To proste zadanie stanowi świetne uzupełnienie dla elementu dialogu, który nie jest animowany. Interakcja jest osiągana przez obserwowanie kliknięć elementu dialogu i wykorzystywanie przekazywania zdarzeń do oceny tego, co zostało kliknięte. Interakcja będzie miała miejsce tylko wtedy, gdy element znajduje się na szczycie: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 podano ciąg znaków. Ten ciąg znaków może być pobierany przez inne skrypty JavaScript, aby uzyskać informacje o tym, jak zamknięto okno dialogowe. Za każdym razem, gdy wywołuję funkcję za pomocą różnych przycisków, dołączam też ciągi znaków do zamykania, aby zapewnić kontekst aplikacji na temat interakcji z użytkownikiem.

Dodawanie zdarzeń zamknięcia i zamkniętych

Element dialogu ma zdarzenie zamknięcia: jest emitowany natychmiast po wywołaniu funkcji dialogu close(). Ponieważ animujemy ten element, dobrze jest mieć zdarzenia przed animacją i po niej, by zmiana mogła pobrać dane lub zresetować formularz okna. Tutaj używam go do zarządzania dodawaniem atrybutu inert w zamkniętym oknie dialogowym, a w tym pokazie używam go do modyfikowania listy awatara, jeśli użytkownik przesłał nowy obraz.

Aby to zrobić, utwórz 2 nowe zdarzenia o nazwach closingclosed. Następnie poczekaj na wbudowane zdarzenie zamknięcia w oknie. Tutaj ustaw okno na inert i wyślij zdarzenie closing. Następnym zadaniem jest oczekiwanie na zakończenie animacji i przejść w dialogu, a potem wysłanie 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 komponencie toast, zwraca obietnicę na podstawie ukończenia obietnic animacji i przejścia. Dlatego dialogClose to asynchroniczna funkcja; może ona następnie await zwrócić obietnicę i bezpiecznie przejść do zamkniętego zdarzenia.

Dodawanie wydarzeń otwarcia i otwartych

Dodanie tych zdarzeń nie jest tak proste, ponieważ wbudowany element dialogu nie udostępnia zdarzenia otwarcia, jak ma to miejsce w przypadku zamknięcia. Używam narzędzia MutationObserver, aby dostarczać statystyk na temat zmian atrybutów okna. W tym obserwatorze będę sprawdzać zmiany atrybutu open i odpowiednio zarządzać zdarzeniami niestandardowymi.

Podobnie jak w przypadku zdarzeń zamykania i zamknięcia utwórz 2 nowe zdarzenia o nazwach openingopened. W tym miejscu, gdzie wcześniej nasłuchiwaliśmy zdarzenia zamknięcia dialogu, użyjemy teraz utworzonego obserwatora mutacji, aby obserwować atrybuty dialogu.


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 zostanie wywołana, gdy zmienią się atrybuty dialogu. Przekaże ona listę zmian jako tablicę. Przejrzyj zmiany atrybutów, aby sprawdzić, czy attributeName jest otwarta. Następnie sprawdź, czy element ma atrybut czy nie – to informuje, czy okno zostało otwarte. Jeśli został otwarty, usuń atrybut inert, a zaznacz element żądający autofocus lub pierwszy element button znaleziony w oknie. Na koniec, podobnie jak w przypadku zdarzenia zamykającego i zamkniętego, od razu wyślij zdarzenie otwierające, poczekaj na zakończenie animacji, a potem wyślij otwarte zdarzenie.

Dodawanie usuniętego zdarzenia

W aplikacjach jednostronicowych okna dialogowe są często dodawane i usuwane w zależności od tras lub innych potrzeb i stanu aplikacji. Wyczyścić zdarzenia lub dane po usunięciu okna dialogowego.

Możesz to osiągnąć za pomocą innego obserwatora mutacji. Tym razem zamiast obserwować atrybuty elementu dialogu, będziemy obserwować elementy podrzędne elementu body i sprawdzać, czy elementy dialogu 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)
      }
    })
  })
})

Wywołanie zwrotne obserwatora mutacji jest wywoływane za każdym razem, gdy elementy podrzędne są dodawane do treści dokumentu lub z nich usuwane. Obserwowane mutacje dotyczą removedNodes, które mająnodeName w dialogu. Jeśli okno zostało usunięte, zdarzenia kliknięcia i zamknięcia są usuwane, by zwolnić pamięć, a niestandardowe usunięte zdarzenie jest wysyłane.

Usunięcie atrybutu wczytywania

Aby zapobiec odtwarzaniu animacji wyjścia okna dialogowego po dodaniu go do strony lub po jej załadowaniu, dodano do okna atrybut wczytywania. Poniższy skrypt czeka na zakończenie animacji dialogu, a potem usuwa atrybut. Teraz dialog może się swobodnie pojawiać i znikać, a my skutecznie ukryliśmy animację, która wcześniej rozpraszała uwagę.

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

Dowiedz się więcej o zapobieganiu animacjom kluczowych klatek podczas wczytywania strony.

Razem

Oto pełna treść dokumentu dialog.js, który został już przez nas omówiony:

// 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 dialogu, który ma 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)

W ten sposób 2 okna dialogowe zostały ulepszone o możliwość ich zamknięcia, naprawiono animacje wczytywania i dodano więcej zdarzeń.

Nasłuchiwanie nowych zdarzeń niestandardowych

Każdy ulepszony element dialogu może teraz odbierać 5 nowych zdarzeń, takich jak:

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 demo, które stworzyłem za pomocą elementu dialogowego, używam tego zamkniętego zdarzenia i danych formularza, aby dodać do listy nowy element awatara. W tym przypadku dialog ma już ukończoną animację wyjścia, a następnie niektóre skrypty są animowane w nowym awatarze. Dzięki nowym zdarzeniom możesz lepiej dostosować działanie aplikacji do potrzeb użytkowników.

Uwaga dialog.returnValue: ten element zawiera ciąg znaków zamknięcia przekazywany po wywołaniu zdarzenia w oknie close(). W przypadku zdarzenia dialogClosed ważne jest, aby wiedzieć, czy okno zostało zamknięte, anulowane czy potwierdzone. Jeśli wynik zostanie potwierdzony, skrypt pobiera wartości formularza i resetuje go. Przydatne jest to, że gdy okno zostanie ponownie wyświetlone, będzie puste i gotowe do przesłania.

Podsumowanie

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂

Zróżnicujemy nasze podejścia i poznamy wszystkie sposoby tworzenia stron internetowych.

Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy utworzone przez społeczność

Zasoby