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ąwyświetl kod źródłowy.

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> 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 jednej stronie zamiast działania na kilku stronach. Może to być spowodowane tym, że formularz jest krótki lub jedynym działaniem wymaganym od użytkownika jest potwierdzenie lub anulowanie.

Element <dialog> stał się ostatnio stabilny we wszystkich przeglądarkach:

Obsługa przeglądarek

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

Źródło

Elementowi brakuje kilku elementów, więc w tym wyzwaniu dotyczącym interfejsu użytkownika dodaję oczekiwane przeze mnie elementy związane z doświadczeniem programisty: dodatkowe zdarzenia, lekkie zamknięcie, animacje niestandardowe oraz typy mini i mega.

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ć ten obraz bazowy.

Element dialogu ma wiele wspólnego z oknem 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”, a dialogi zostały 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 dialogowych są tworzone razem. Warto użyć elementu formularza, aby otoczyć nim treść dialogu, dzięki czemu JavaScript będzie mieć 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ł modalu i przycisk zamknięcia. 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 element 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ędzie on mniejszy i będzie się lepiej mieścić w wierszu.

<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 się na odpowiednich przyciskach otwarcia i zamknięcia. Gdy otworzy się pasek boczny, fokus zostanie ustawiony na przycisku Zamknij. 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ć. W sekcji JavaScript przywrócę tę funkcję.

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 wszystkie części dokumentu mogą być „zamrożone”, co oznacza, że nie są już elementami skupienia ani nie można ich klikać myszką. Zamiast blokowania punktu skupienia, punkt skupienia jest kierowany na jedyną interaktywną część dokumentu.

Otwieranie elementu i automatyczne ustawianie ostrości

Domyślnie element dialogu przypisuje fokus do pierwszego elementu, który można zaznaczyć w oznaczeniu dialogu. 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 potwierdzenie jest celowe, a nie przypadkowe.

Zamknij za pomocą 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 łatwy sposób na nadanie stylów elementowi okna dialogowego oraz trudny sposób. Łatwy sposób polega na nie zmienianiu właściwości wyświetlania okna dialogowego i pracy z jego ograniczeniami. Aby zapewnić animacje niestandardowe otwierania i zamykania okna dialogowego, przejęcia właściwości display i innych, wybrałem trudniejszą ś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 niektóre przyciski, które Open Props udostępnia 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>

Własność usługi wyświetlania

Domyślne zachowanie wyświetlania i ukrywania elementu dialogu przełącza właściwość wyświetlania z block na none. Oznacza to, że nie można animacji w obrębie tego elementu, tylko tylko na nim. 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 stan 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;
}

Okno jest teraz niewidoczne i nie można z nim wchodzić w interakcje, gdy jest zamknięte. 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 z motywami jasnym i ciemnym, pokazujące kolory powierzchni.

Chociaż color-scheme przełącza dokument na motyw 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 jego 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, a na ekranie komputera nie będzie na tyle szerokie, aby było trudne do odczytania. Następnie dodaję max-block-size, aby okno nie przekraczało wysokości strony. Oznacza to też, że musimy określić, gdzie znajduje się obszar przewijania okna dialogowego, na wypadek, gdyby był 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;
}

Zauważyłaś, że max-block-size jest podwójnie? Pierwszy z nich używa wartości 80vh, czyli fizycznej jednostki widoku. Chcę zachować dialog w ramach względnego przepływu dla użytkowników z całego świata, więc w drugiej deklaracji używam logicznej, nowszej i tylko częściowo obsługiwanej jednostki dvb, gdy stanie się ona stabilniejsza.

Pozycjonowanie okna Mega

Aby ułatwić sobie pozycjonowanie elementu okna dialogowego, warto podzielić go na 2 części: tło na pełnym ekranie i kontener okna dialogowego. Tło musi zakrywać wszystko, zapewniając efekt przyciemnienia, który pomaga zrozumieć, że ten dialog jest na pierwszym planie, a treści z tła są niedostępne. 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 moduł okienkowy 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ę devtools na marginesy w dialogu mega 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 starannie przygotowanych elementów sceny firmy 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);
}

W przyszłości przeglądarki mogą umożliwić przejścia dla elementu tła.backdrop-filter

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

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

Dodatki do stylizacji

Nazywam tę sekcję „Dodatki”, ponieważ dotyczy ona bardziej elementu dialogu w moim demo niż elementu dialogu w ogóle.

ograniczenie przewijania,

Gdy wyświetla się okno dialogowe, użytkownik może nadal 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 przewijania, 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;
}

Gdy teraz otworzysz mega okno, dokument HTML będzie 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 okna dialogowego. Ustawienie tej stałej wysokości i stałego rozmiaru wiersza pozwala ograniczyć element artykułu i umożliwia przewijanie, gdy element jest za długi:

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

Zrzut ekranu pokazujący narzędzia programistyczne nakładają informacje o układzie siatki na wiersze.

Stylizacja okna dialogowego <header>

Rolą tego elementu jest podanie tytułu treści dialogu i zaproponowanie łatwego do znalezienia przycisku Zamknij. Ma też przypisany kolor, dzięki któremu wydaje się, że znajduje się za 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 nakładanie się informacji o układzie flexbox w Narzędziach deweloperskich w Chrome na nagłówek okna dialogowego

Nadawanie stylu przyciskowi zamykania nagłówka

Ponieważ w tym pliku demonstracyjnym użyto przycisków Open Props, przycisk zamknięcia został dostosowany do okrągłej ikony:

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ę Narzędzi deweloperskich w Chrome z informacjami o rozmiarach i odstępach przycisku zamykania nagłówka

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

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 nakładanie się informacji flexboxu na elementy menu w stopce w Narzędziach dla programistów w Chrome

Animacja

Elementy dialogu są często animowane, ponieważ pojawiają się w oknie i z niego znikają. Dodanie ruchu do dialogów podczas wchodzenia i wychodzenia z kadru pomaga użytkownikom łatwiej się zorientować w treści.

Zazwyczaj element dialogu może być animowany tylko w kierunku w górę, a nie w dół. Dzieje się tak, ponieważ przeglądarka przełącza w elemencie właściwość display. Wcześniej przewodnik ustawiał wyświetlanie na siatkę i nigdy nie ustawiał go 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 przyciemnienie i rozjaśnienie.
  2. Jeśli ruch jest prawidłowy, dodawane są animacje przesunięcia i powiększenia.
  3. Responsywny układ mobilny megaokna jest dostosowany do przesuwania.

Bezpieczne i sensowne domyślne przejście

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ść od 0% do 100%, powiedz przeglądarce, jak długo i jakie łagodne przejście chcesz uzyskać:

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 w dół podczas znikania. Możesz to zrobić za pomocą zapytania o multimedia prefers-reduced-motion i kilku komponentów 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 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, ale 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
}

Te dodatki wynikają z potrzeby łagodnego zamykania (kliknięcie tła okna dialogowego), animacji i kilku dodatkowych zdarzeń, które mają na celu lepsze dopasowanie czasu pobierania danych z 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 przekazuje 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ę z różnych przycisków, dołączam też ciągi znaków 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ż ten element jest animowany, warto mieć zdarzenia przed i po animacji, aby można było zmienić dane lub zresetować formularz dialogu. 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 nasłuchuj wbudowanego zdarzenia zamknięcia okna. 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 zdarzenia MutationObserver, aby uzyskać informacje o zmianach atrybutów dialogu. 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 po zmianie atrybutów dialogu. Przekaże ona listę zmian jako tablicę. Przejrzyj zmiany atrybutu, szukając elementu attributeName, który powinien być otwarty. Następnie sprawdź, czy element ma atrybut: informuje on, czy okno zostało otwarte. Jeśli zostało otwarte, usuń atrybut inert, ustaw fokus na elemencie wymagającym autofocus lub na pierwszym elemencie button znalezionym w dialogu. Na koniec, podobnie jak w przypadku zdarzeń zamykania i zamykania, wyślij od razu zdarzenie otwarcia, zaczekaj, aż animacja się zakończy, a potem wyślij zdarzenie otwarcia.

Dodawanie usuniętego zdarzenia

W przypadku aplikacji jednostronicowych dialogi są często dodawane i usuwane na podstawie tras lub innych potrzeb i stanu aplikacji. Może to być przydatne, gdy usuniesz okno.

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

Callback funkcji obserwatora mutacji jest wywoływany za każdym razem, gdy do treści dokumentu są dodawane lub usuwane elementy podrzędne. Obserwowane mutacje dotyczą removedNodes, które mająnodeName w dialogu. Jeśli dialog został usunięty, aby zwolnić pamięć, są usuwane zdarzenia kliknięcia i zamknięcia, a zamiast nich wysyłane jest zdarzenie niestandardowe usunięcia.

Usuwanie atrybutu loading

Aby zapobiec odtwarzaniu animacji wyjścia okna dialogowego po dodaniu go do strony lub po jej załadowaniu, dodano do okna atrybut ładowania. Poniższy skrypt czeka na zakończenie animacji dialogu, a potem usuwa atrybut. Teraz dialog może być animowany, a nieruchoma animacja została ukryta.

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

Więcej informacji o zapobieganiu animacjom kluczowych klatek podczas wczytywania strony znajdziesz tutaj.

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 animują nowy awatar. Dzięki nowym zdarzeniom możesz lepiej dostosować działanie aplikacji do potrzeb użytkowników.

Notice dialog.returnValue: zawiera ciąg tekstowy zamykania przekazywany podczas wywołania zdarzenia dialog close(). W przypadku zdarzenia dialogClosed ważne jest, aby wiedzieć, czy okno zostało zamknięte, anulowane czy potwierdzone. Jeśli tak, skrypt pobiera wartości formularza i go resetuje. 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 społeczności

Zasoby