Tworzenie komponentu okna

Podstawowe informacje o tworzeniu mini i megamodalnych mini i megamodalnych reklam adaptacyjnych kolorów, które dostosowują się do kolorów i dopasowują się do kolorów za pomocą elementu <dialog>.

W tym poście chcę opowiedzieć, jak za pomocą elementu <dialog> tworzyć elastyczne i łatwo dostępne mini i megamodalne panele modalne, które dostosowują się do kolorów. Wypróbuj wersję demonstracyjną i zobacz źródło.

Prezentacja dużych i minimalistycznych okien w ciemnych i jasnych motywach.

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

Przegląd

Element <dialog> świetnie nadaje się do umieszczania na stronie informacji kontekstowych lub działań. Zastanów się, w jakiej sytuacji dla użytkownika może być korzystne wykonanie tego samego działania na stronie zamiast działania na wielu stronach: może to być spowodowane tym, że formularz jest mały, albo jedynym wymaganym działaniem użytkownika jest potwierdzenie lub anulowanie.

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

Obsługa przeglądarek

  • 37
  • 79
  • 98
  • 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 miniaturowy i megatypowy element.

Markup

Podstawowe elementy elementu <dialog> są skromne. Element zostanie automatycznie ukryty i ma wbudowane style, które nakładają się na treść.

<dialog>
  …
</dialog>

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

Tradycyjnie elementy dialogowe mają wiele wspólnego z modalnym, a nazwy są często wymienne. Udało mi się wykorzystać element dialogowy zarówno w małych okienkach (mini), jak i na całej stronie (mega). Nazwałam je „mega” i „mini”, które nieco przystosowały się do różnych zastosowań. Dodałem atrybut modal-mode, aby umożliwić określenie typu:

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

Zrzut ekranu przedstawiający okna miniatury i duże okna w motywie jasnym i ciemnym.

Nie zawsze, ale zwykle do zbierania informacji o interakcji służą elementy dialogowe. Formularze w elementach dialogów ze sobą współpracują. Warto użyć elementu formularza, który otacza treść okna, tak aby JavaScript mógł uzyskać dostęp do danych wpisanych przez użytkownika. Ponadto przyciski w formularzu korzystające z funkcji 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>

Mega okno

Duże okno ma 3 elementy: <header>, <article> i <footer>. Służą one jako kontenery semantyczne, a także cele stylu prezentacji okna. Nagłówek zawiera tytuł i zawiera przycisk zamykania. Ten artykuł dotyczy wprowadzania danych i informacji w formularzu. Stopka zawiera <menu> przycisków poleceń.

<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 zawiera wbudowany moduł obsługi zdarzeń autofocus i onclick. Po otwarciu okna zostanie zaznaczony atrybut autofocus. Według mnie sprawdzoną metodą jest umieszczenie go na przycisku anulowania, a nie na przycisku potwierdzenia. Dzięki temu potwierdzenie jest celowe i nie przypadkowe.

Miniokno

Małe okno jest bardzo podobne do megaokna, ale brakuje w nim elementu <header>. Dzięki temu będą mniejsze i lepiej będą umieszczone w tekście.

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

Okno to solidny fundament dla elementu zajmującego cały widoczny obszar, który może zbierać dane i informacje o interakcjach użytkowników. Te podstawowe elementy mogą sprawić, że w Twojej witrynie lub aplikacji możesz tworzyć bardzo interesujące i skuteczne interakcje.

Ułatwienia dostępu

Okno ma wbudowane bardzo dobre ułatwienia dostępu. Zamiast dodawać nowe funkcje jak zwykle, wiele z nich już tu jest.

Przywracam fokus

Tak jak w przypadku tworzenia komponentu sidenav, ważne jest, by prawidłowe otwarcie 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 zaznaczenie zostanie przywrócone do przycisku, który go otworzył.

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

Jeśli chcesz animować okno dialogowe i oddalać się od niego, ta funkcja nie będzie dostępna. Przywrócę tę funkcję w sekcji JavaScript.

Traktowanie fokusu

Element okna zarządza tagiem inert w dokumencie. Przed inert używano JavaScriptu do sprawdzania, czy element opuszcza element, w którym następuje przechwycenie i włożenie go z powrotem.

Obsługa przeglądarek

  • 102
  • 102
  • 112
  • 15.5

Źródło

Po inert dowolna część dokumentu może zostać „zablokowana” tak, że nie będzie już zaznaczać elementów docelowych lub jest interaktywna po użyciu myszy. Zamiast uwodnić fokus, uwaga jest skupiona na jedynej interaktywnej części dokumentu.

Otwieranie i automatyczne ustawianie ostrości elementu

Domyślnie element okna aktywuje pierwszy element, który można zaznaczyć, w znacznikach okna. Jeśli nie jest to najlepszy element, który użytkownik powinien wybrać, użyj atrybutu autofocus. Jak pisaliśmy wcześniej, zalecam naciśnięcie przycisku anulowania, a nie przycisku potwierdzenia. Dzięki temu potwierdzenie jest celowe i nie przypadkowe.

Zamykanie klawisza Escape

Ważne jest, by ułatwić zamknięcie tego potencjalnie zakłócającego elementu. Na szczęście element okna zajmie się klawiszem Escape, co pozwoli Ci uwolnić się od obowiązków administracyjnych.

Style

Istnieje prosty sposób na określenie stylu elementu dialogowego i sztywnej ścieżki. Łatwą ścieżkę można osiągnąć, nie zmieniając właściwości wyświetlania okna i stosując się do jego ograniczeń. Przygotowuję animacje niestandardowe do otwierania i zamykania okna, przejmując między innymi właściwość display.

Stylizacja z otwartymi rekwizytami

Aby przyspieszyć dostosowywanie kolorów i ogólną spójność projektu, bez wstydu skorzystałem z 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. Importy te pomagają mi skupić się na dostosowywaniu okna dialogowego i wersji demonstracyjnej, a jednocześnie nie potrzebują wielu stylów do jego obsługi i poprawienia wyglądu.

Określanie stylu 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. Najpierw ustaw własną właściwość display:

dialog {
  display: grid;
}

Zmiana wartości właściwości wyświetlania i tym samym jej posiadanie, jak pokazano powyżej we fragmencie kodu CSS, wymaga zarządzania sporą liczbą stylów, aby zapewnić użytkownikom odpowiedni komfort. Po pierwsze, domyślny stan okna jest zamknięty. Możesz przedstawić ten stan wizualnie i uniemożliwić okno o odbieraniu 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 JavaScript do zarządzania atrybutem inert w oknie. Dzięki temu użytkownicy klawiatury i czytnika ekranu nie będą mieli dostępu do ukrytego okna.

Nadanie dialogowi adaptacyjnego motywu kolorystycznego

Mega okno z jasnym i ciemnym motywem prezentujące kolory powierzchni.

Usługa color-scheme włącza w dokumencie dostępny w przeglądarce adaptacyjny motyw kolorystyczny do ustawień systemu jasnego i ciemnego, ale chciałem(-am) bardziej dostosować element okna. W otwartych rekwizytach dostępnych jest kilka kolorów powierzchni, które automatycznie dostosowują się do preferencji systemu jasnego i ciemnego tła, podobnie jak w przypadku color-scheme. Świetnie nadają się do tworzenia warstw w projekcie. Uwielbiam używać kolorów, które pomagają wizualnie wzmocnić efekt powierzchni warstw. Kolor tła to var(--surface-1). Aby umieścić ją na tej warstwie, użyj koloru var(--surface-2):

dialog {
  …
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

Później dodamy więcej kolorów adaptacyjnych w przypadku elementów podrzędnych, takich jak nagłówek i stopka. Traktuję je jako dodatkowe elementy okna, ale są naprawdę ważne, aby były atrakcyjne i dobrze zaprojektowane.

Elastyczny rozmiar okien

Domyślnie okno przekazuje rozmiar do zawartości, co jest zwykle najlepsze. Moim celem jest ograniczenie rozmiaru max-inline-size do czytelnego rozmiaru (--size-content-3 = 60ch) lub 90% szerokości widocznego obszaru. Dzięki temu okno nie będzie się rozciągało od krawędzi do krawędzi na urządzeniu mobilnym ani nie będzie tak szerokie na ekranie komputera, że będzie trudno je przeczytać. Potem dodam 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;
}

Czy widzisz, że aplikacja max-block-size jest wyświetlana 2 razy? Pierwszy korzysta z 80vh, czyli fizycznego obszaru widocznego. 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 megaokna

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 okna może dowolnie wyśrodkować się na tym tle i przybierać dowolny kształt, którego wymaga jego zawartość.

Te style naprawiają element okna w oknie przez rozciągnięcie go do każdego roga i wykorzystanie margin: auto do wyśrodkowania treści:

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

W małych widocznych obszarach trochę inaczej określam styl tego megamodalnego całej strony. Ustawiam dolny margines na 0, co umieszcza zawartość okna na dole widocznego obszaru. Dzięki kilku korektom stylu mogę przekształcić okno dialogowe w arkusz działań bliżej użytkowników:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Zrzut ekranu przedstawiający narzędzia deweloperskie nakładające odstępy między marginesami w oknie megapiksela na komputerze i komórce po otwarciu.

Pozycjonowanie miniokna

Gdy używam większego widocznego obszaru, np. na komputerze, zdecydowałem się na umieszczenie miniokna nad elementem, który je wywołał. Potrzebuję do tego JavaScriptu. Metodę, którą wykorzystuję, znajdziesz tutaj, ale moim zdaniem wykracza ona poza zakres tego artykułu. Bez JavaScriptu miniokno pojawi się na środku ekranu, tak jak duże okno.

Wyróżnij się

Na koniec dodaj blasku okno, aby wyglądało jak miękka powierzchnia znajdująca się daleko nad stroną. Zmniejsza to miękkość obrazu za pomocą zaokrąglania rogów okna. Głębia odkrywa się dzięki jednej ze starannie dopracowanych rekwizytów z cieniem:

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

Dostosowywanie pseudoelementu tła

Postanowiłam pracować bardzo delikatnie z tłem, dodając tylko efekt rozmycia za pomocą funkcji backdrop-filter w ogromnym oknie:

Obsługa przeglądarek

  • 76
  • 79
  • 103
  • 9

Ź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 przedstawiający megaokna z rozmytym tłem z kolorowymi awatarami.

Dodatki do stylu

Nazywamy tę sekcję „extras”, ponieważ ma ona więcej wspólnego z demonstracją elementu dialogowego niż z ogólnym elementem interfejsu.

Ograniczenia dotyczące przewijania

Po wyświetleniu okna użytkownik nadal może przewijać stronę za nim, co jest mi niepotrzebne:

Normalnie byłaby to opcja overscroll-behavior, ale zgodnie ze specyfikacją nie ma to wpływu na okno, ponieważ nie jest to port do przewijania, tzn. nie jest elementem do przewijania, więc nie ma sensu zapobiegać. Mogę użyć JavaScriptu, by śledzić nowe zdarzenia z tego przewodnika, takie jak „zamknięte” i „otwarte”, oraz przełączyć overflow: hidden w dokumencie lub poczekać, aż :has() będzie stabilny 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 megaokna dokument HTML zawiera overflow: hidden.

Układ <form>

Jest on nie tylko bardzo ważny, ponieważ przy zbieraniu informacji o interakcji użytkownika używa go do określania elementów nagłówka, stopki i artykułu. W tym układzie chcę przedstawić element podrzędny artykułu jako obszar, który można przewijać. Robię to dzięki grid-template-rows. Elementowi artykułu przypisano atrybut 1fr, a sam formularz ma taką samą maksymalną wysokość jak element okna. Ustawienie takiej wysokości i stałego rozmiaru wiersza sprawia, że element artykułu może być ograniczony i przewijany, gdy przekroczy jego zawartość:

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.

Określanie stylu 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 obejmują kontener typu flexbox, elementy wyrównane pionowo z krawędziami oraz pewne dopełnienia i luki, które zapewniają trochę miejsca na tytuł i przycisk 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.

Określanie stylu przycisku zamykania nagłówka

Ponieważ w wersji demonstracyjnej są używane przyciski Otwarte rekwizyty, przycisk zamykania zmienia się w okrągły, skupiony na ikonie przycisk:

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 przedstawiający narzędzia deweloperskie w Chrome z nałożonymi informacjami o rozmiarze i dopełnieniu przycisku zamykania nagłówka.

Określanie stylu okna dialogowego <article>

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

W tym celu nadrzędny element formularza określił dla siebie pewne maksymalne wartości, które ograniczają, że ten element artykułu będzie zbyt wysoki. Ustaw overflow-y: auto tak, aby paski przewijania były wyświetlane tylko wtedy, gdy są potrzebne, i zawierały przewijanie w obrębie znacznika overscroll-behavior: contain, a pozostałe to niestandardowe style 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 ma zawierać menu z przyciskami polecenia. Pole Flexbox służy do wyrównania zawartości do końca wbudowanej osi stopki i do uzyskania odstępów między przyciskami.

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 narzędzia deweloperskie w Chrome z nałożonymi informacjami o układzie flexbox na elemencie stopki.

Element menu zawiera przyciski poleceń w oknie. Wykorzystuje układ Flexbox zawijany z elementem gap, który zapewnia odstęp między przyciskami. Elementy menu są dopełnione, 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 okien są często animowane, ponieważ otwierają i zamykają okno. Zachęcanie użytkowników do ruchu w ramach ruchu przychodzącego i wyjścia pomaga użytkownikom zorientować się w przepływie ruchu.

Zwykle element okna może być animowany tylko wewnątrz, ale nie na zewnątrz. Dzieje się tak, ponieważ przeglądarka przełącza właściwość display elementu. Wcześniej przewodnik ustawił wyświetlanie na siatkę, ale nigdy nie ustawiał jej na brak. Odblokowuje to możliwość wchodzenia i wycofywania animacji.

Open Props zawiera wiele animacji w klatce kluczowej, dzięki czemu organizowanie jest łatwe i czytelne. Oto moje cele animacji i warstwowe podejście:

  1. Zmniejszony ruch to domyślne przejście, proste przejście od nieprzezroczystości, które pojawia się lub znika.
  2. Jeśli ruch jest prawidłowy, zostaną dodane animacje przesuwania i skalowania.
  3. Elastyczny układ megaokna na urządzenia mobilne jest dostosowany do wysuwania.

bezpieczne i wartościowe przeniesienie domyślne;

Otwarte rekwizyty oferują klatki kluczowe do wyłaniania i znikania, ale preferuję wielowarstwowe podejście do przejść jako domyślne z animacjami klatek kluczowych jako potencjalnym ulepszeniem. Wcześniej określiliśmy styl widoczności okna z przezroczystością, administrują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 rodzaj wygładzania chcesz uzyskać:

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

Dodawanie ruchu do przejścia

Jeśli użytkownikowi nie przeszkadza ruch, duże okno dialogowe i minimalizacja powinny wysunąć się w górę jako wejście, a rozwijać się w górę. 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;
  }
}

Dostosowanie animacji wyjściowej do wyświetlania na urządzeniach mobilnych

Wcześniej ten styl został przystosowany do urządzeń mobilnych, by przypominał arkusz działań – taki jak mały kawałek papieru wysunął się z dołu ekranu i nadal przylega do dołu. Animacja wyjścia ze skalowania w poziomie nie pasuje dobrze do nowego projektu, więc możemy ją dostosować, używając kilku zapytań o multimedia i kilku otwartych rekwizytó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 JavaScripcie jest jeszcze kilka rzeczy, które można dodać:

// 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.

Dodaję oświetlenie zamknięte

To proste zadanie i świetne uzupełnienie elementu dialogowego, który nie jest animowany. Interakcja jest osiągana przez obserwowanie kliknięć w oknie dialogowym i wykorzystanie dymków zdarzeń do oceny kliknięcia. close() sprawdza tylko, czy 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')
}

Powiadomienie: dialog.close('dismiss'). Zdarzenie jest wywoływane i podano ciąg znaków. Ten ciąg może być pobierany przez inny kod JavaScript, aby uzyskać wgląd w to, jak zostało zamknięte okno. Za każdym razem, gdy wywołam funkcję z poziomu różnych przycisków, podano też zbliżony tekst, aby przedstawić w aplikacji kontekst dotyczący interakcji użytkownika.

Dodaję zdarzenia zamykające i zamknięte

Element okna zawiera zdarzenie zamknięcia, które jest wysyłane natychmiast po wywołaniu funkcji okna dialogowego close(). Ponieważ animujemy ten element, dobrze jest mieć zdarzenia przed animacją i po niej, by zmiana mogła pobrać dane lub zresetować formularz okna. Używam go tutaj do zarządzania dodaniem atrybutu inert w zamkniętym oknie, a w wersji demonstracyjnej do modyfikowania listy awatarów, jeśli użytkownik przesłał nowe zdjęcie.

W tym celu utwórz 2 nowe zdarzenia o nazwach closing i closed. Następnie poczekaj na wbudowane zdarzenie zamknięcia w oknie. W tym miejscu ustaw okno na inert i wyślij zdarzenie closing. Następne zadanie to zaczekać na zakończenie działania animacji i przejść w oknie, a następnie 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 też używana w tworzeniu komponentu toast, zwraca obietnicę zależną od ukończenia animacji i obietnic przejścia. Dlatego dialogClose jest funkcją asynchroniczną. Dzięki temu może await zwrócić obietnicę i bez obaw przejść do zdarzenia zamkniętego.

Dodawanie otwieranych i otwartych zdarzeń

Dodanie tych zdarzeń nie jest tak proste, ponieważ wbudowany element okna nie obsługuje zdarzeń otwartych, tak jak w przypadku zamknięcia. Używam narzędzia MutationObserver, aby dostarczać statystyk na temat zmian atrybutów okna. W tym obserwatorze będę obserwować zmiany w atrybucie otwartym i odpowiednio zarządzać zdarzeniami niestandardowymi.

Podobnie jak w przypadku rozpoczęcia zamykania i zakończenia wydarzeń, utwórz 2 nowe wydarzenia o nazwach opening i opened. Tam, gdzie wcześniej nasłuchiwaliśmy zdarzenia zamknięcia okna, tym razem użyjemy utworzonego narzędzia do obserwowania mutacji, aby obserwować atrybuty okna.

…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  …
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

Po zmianie atrybutów dialogowych funkcja wywołania zwrotnego obserwatora mutacji jest wywoływana i podaje listę zmian w postaci tablicy. Przeprowadź iterację zmian atrybutów i poszukaj otwartego pola attributeName. 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 wydarzenia

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ąć, korzystając z innego obserwatora mutacji. Tym razem zamiast obserwować atrybuty w elemencie dialogowym, będziemy obserwować elementy podrzędne elementu body i usuwać 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 elementy podrzędne są dodawane do treści dokumentu lub z nich usuwane. Obserwowane mutacje dotyczą danych typu removedNodes, które mają właściwość nodeName okna. 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 uniemożliwić odtwarzanie animacji wyjściowej okna dialogowego po dodaniu do strony lub jej wczytania, dodaliśmy do tego okna atrybut wczytywania. Poniższy skrypt czeka na zakończenie odtwarzania animacji okien i usuwa atrybut. Okno można teraz swobodnie poruszać się po ekranie i animować, a efektywnie ukryliśmy rozpraszające animacje.

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

Więcej informacji o zapobieganiu pojawianiu się animacji klatek kluczowych podczas wczytywania strony znajdziesz tutaj.

Wszystko razem

Oto dialog.js w całości, ponieważ już objaśniliś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 do elementu okna, do którego mają zostać dodane te nowe zdarzenia i funkcje:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

W obu 2 oknach udoskonalono też funkcję lekkiego zamykania, poprawki wczytywania animacji i dodatkowe zdarzenia, z którymi można pracować.

Nasłuchiwanie nowych zdarzeń niestandardowych

Każdy uaktualniony element okna może 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 utworzonej przeze mnie wersji demonstracyjnej za pomocą elementu okna użyję zdarzenia zamkniętego i danych formularza, aby dodać do listy nowy element awatara. Jest to dobre czas, ponieważ okno dialogowe zakończyło już animację wyjścia, a część skryptów została animowana w nowym awatarze. Dzięki nowym wydarzeniom administracja wrażeniami użytkownika może przebiegać płynniej.

Uwaga dialog.returnValue: ten element zawiera ciąg znaków zamknięcia przekazywany po wywołaniu zdarzenia w oknie close(). W zdarzeniu dialogClosed bardzo 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. Dzięki temu po ponownym wyświetleniu okno jest puste i gotowe do przesłania nowego pliku.

Podsumowanie

Wiesz już, jak to zrobiłem. Jak Ty? 🙂

Stosujmy różne podejścia i poznajmy sposoby budowania obecności w internecie.

Przygotuj wersję demonstracyjną, a potem dodam linki do tweetów, a ja dodam ją do poniższej sekcji na temat remiksów na karcie Społeczność.

Remiksy utworzone przez społeczność

Zasoby