Tworzenie komponentu toastu

Podstawowe informacje o tym, jak utworzyć adaptacyjny i dostępny komponent powiadomienia.

W tym poście chcę podzielić się przemyśleniami na temat tworzenia komponentu powiadomienia. Wypróbuj wersję demonstracyjną.

Wersja demonstracyjna

Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:

Przegląd

Toasty to nieinteraktywne, pasywne i asynchroniczne krótkie wiadomości dla użytkowników. Zwykle służą one jako wzorzec informacji zwrotnych w interfejsie, który informuje użytkownika o wynikach działania.

Interakcje

Komunikaty typu toast różnią się od powiadomień, alertówpromptów, ponieważ nie są interaktywne – nie można ich odrzucić ani zachować. Powiadomienia służą do przekazywania ważniejszych informacji, wiadomości synchronicznych, które wymagają interakcji, lub wiadomości na poziomie systemu (w przeciwieństwie do wiadomości na poziomie strony). Wyskakujące okienka są mniej aktywne niż inne strategie powiadamiania.

Znacznik

Element <output> jest dobrym wyborem w przypadku wyskakujących powiadomień, ponieważ jest odczytywany przez czytniki ekranu. Prawidłowy kod HTML stanowi bezpieczną podstawę, którą możemy rozszerzyć za pomocą JavaScriptu i CSS. Będzie dużo JavaScriptu.

toast,

<output class="gui-toast">Item added to cart</output>

Możesz zwiększyć role="status". Zapewnia to rezerwę, jeśli przeglądarka nie przypisze elementom <output> domyślnej roli zgodnie ze specyfikacją.

<output role="status" class="gui-toast">Item added to cart</output>

Pojemnik na tosty

Jednocześnie może być wyświetlanych więcej niż 1 komunikat. Do koordynowania wielu komunikatów typu toast służy kontener. Ten kontener obsługuje też pozycję wyskakujących okienek na ekranie.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Układy

Wybrałem przypinanie powiadomień do inset-block-end obszaru widocznego i jeśli dodam więcej powiadomień, będą one ułożone w stos od tej krawędzi ekranu.

Kontener GUI

Kontener powiadomień wykonuje całą pracę związaną z układem, aby wyświetlać powiadomienia. Jest on fixed do obszaru widoku i używa właściwości logicznej inset, aby określić, do których krawędzi ma być przypięty, oraz niewielkiego padding od tej samej krawędzi block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Zrzut ekranu z rozmiarem i wypełnieniem elementu .gui-toast-container w Narzędziach deweloperskich.

Oprócz pozycjonowania w widocznym obszarze kontener powiadomień jest kontenerem siatki, który może wyrównywać i rozmieszczać powiadomienia. Elementy są wyśrodkowane jako grupa za pomocą justify-content i indywidualnie za pomocą justify-items. Dodaj trochę gap, aby tosty się nie stykały.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Zrzut ekranu z nałożoną siatką CSS na grupę toastów, tym razem z zaznaczeniem odstępów i przerw między elementami podrzędnymi toastów.

Toast GUI

Pojedynczy toast ma pewne padding, bardziej miękkie rogi z border-radius i funkcję min(), która pomaga w dostosowywaniu rozmiaru na urządzeniach mobilnych i komputerach. Elastyczny rozmiar w tym kodzie CSS zapobiega rozszerzaniu się wyskakujących okienek powyżej 90% widocznego obszaru lub 25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Zrzut ekranu przedstawiający pojedynczy element .gui-toast z wyświetlonymi dopełnieniem i promieniem obramowania.

Style

Po ustawieniu układu i położenia dodaj CSS, który pomoże dostosować się do ustawień i interakcji użytkownika.

Pojemnik na tosty

Toast nie jest interaktywny – kliknięcie ani przesunięcie nie powoduje żadnej reakcji, ale obecnie przetwarza zdarzenia wskaźnika. Zapobiegaj przechwytywaniu kliknięć przez wyskakujące okienka za pomocą tego kodu CSS.

.gui-toast-group {
  pointer-events: none;
}

Toast GUI

Nadaj powiadomieniom jasny lub ciemny motyw adaptacyjny z własnymi właściwościami, HSL i zapytaniem o preferencje dotyczące mediów.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animacja

Nowy komunikat powinien pojawić się na ekranie z animacją. Domyślnie ustawia się wartości translate na 0, ale wartość ruchu można zaktualizować do długości w zapytaniu o media preferencji ruchu . Animacja jest wyświetlana wszystkim użytkownikom, ale tylko niektórzy z nich widzą, jak komunikat przesuwa się na pewną odległość.

Oto klatki kluczowe użyte w animacji powiadomienia. CSS będzie kontrolować pojawianie się, oczekiwanie i zamykanie powiadomienia w ramach jednej animacji.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Element powiadomienia następnie konfiguruje zmienne i koordynuje klatki kluczowe.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

Gdy style i kod HTML dostępny dla czytników ekranu są gotowe, JavaScript jest potrzebny do tworzenia, dodawania i usuwania powiadomień na podstawie działań użytkownika. Korzystanie z komponentu powiadomienia powinno być proste i wygodne dla deweloperów, np. tak:

import Toast from './toast.js'

Toast('My first toast')

Tworzenie grupy powiadomień i powiadomień

Gdy moduł powiadomienia wczytuje się z JavaScriptu, musi utworzyć kontener powiadomienia i dodać go do strony. Element został dodany przed body, co zmniejsza prawdopodobieństwo problemów z układaniem z-index, ponieważ kontener znajduje się nad kontenerem wszystkich elementów treści.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Zrzut ekranu przedstawiający grupę wyskakujących okienek między tagami head i body.

Funkcja init() jest wywoływana wewnętrznie w module, a element jest przechowywany jako Toaster:

const Toaster = init()

Element HTML powiadomienia jest tworzony za pomocą funkcji createToast(). Funkcja wymaga tekstu, który ma być wyświetlany w powiadomieniu, tworzy element <output>, dodaje do niego klasy i atrybuty, ustawia tekst i zwraca węzeł.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Zarządzanie jednym lub wieloma komunikatami

JavaScript dodaje teraz do dokumentu kontener, w którym będą wyświetlane powiadomienia, i jest gotowy do dodawania utworzonych powiadomień. Funkcja addToast() koordynuje obsługę jednego lub wielu komunikatów. Najpierw sprawdza liczbę komunikatów i czy ruch jest w porządku, a potem wykorzystuje te informacje, aby dodać komunikat lub wykonać animację, dzięki której inne komunikaty „zrobią miejsce” na nowy.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Podczas dodawania pierwszego powiadomienia Toaster.appendChild(toast) dodaje powiadomienie do strony, wywołując animacje CSS: animacja wejścia, oczekiwanie 3s, animacja wyjścia. flipToast() jest wywoływana, gdy istnieją już powiadomienia typu toast. Wykorzystuje technikę o nazwie FLIP, którą opisał Paul Lewis. Chodzi o obliczenie różnicy w położeniu kontenera przed dodaniem nowego powiadomienia i po jego dodaniu. Wyobraź sobie, że zaznaczasz, gdzie jest teraz toster, gdzie będzie, a potem animujesz jego ruch z jednego miejsca do drugiego.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

Układ jest tworzony za pomocą siatki CSS. Gdy dodasz nowy toast, siatka umieści go na początku i rozmieści w odpowiedniej odległości od pozostałych. W tym czasie animacja internetowa jest używana do animowania kontenera z poprzedniej pozycji.

Łączenie wszystkich elementów JavaScript

Gdy wywoływana jest funkcja Toast('my first toast'), tworzony jest komunikat, który jest dodawany do strony (może nawet kontener jest animowany, aby pomieścić nowy komunikat), zwracana jest obietnica, a utworzony komunikat jest obserwowany pod kątem zakończenia animacji CSS (3 animacji klatek kluczowych) w celu rozwiązania obietnicy.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

Uważam, że najbardziej mylące w tym kodzie są funkcja Promise.allSettled() i mapowanie toast.getAnimations(). Ponieważ w przypadku wyskakującego okienka użyłem kilku animacji klatek kluczowych, aby mieć pewność, że wszystkie się zakończyły, każda z nich musi zostać wywołana z JavaScriptu, a każda z nich musi mieć finished obserwowane obietnice zakończenia. allSettled działa w ten sposób, że po spełnieniu wszystkich obietnic zostaje uznana za zakończoną. Użycie await Promise.allSettled() oznacza, że następny wiersz kodu może bezpiecznie usunąć element i założyć, że toast zakończył swój cykl życia. Wywołanie resolve() spełnia obietnicę Toast na wysokim poziomie, dzięki czemu programiści mogą po wyświetleniu powiadomienia wyczyścić kod lub wykonać inne czynności.

export default Toast

Na koniec funkcja Toast jest eksportowana z modułu, aby inne skrypty mogły ją importować i używać.

Korzystanie z komponentu Toast

Aby użyć powiadomienia lub jego funkcji dla deweloperów, zaimportuj funkcję Toast i wywołaj ją za pomocą ciągu tekstowego wiadomości.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Jeśli deweloper chce wykonać jakieś czynności po wyświetleniu powiadomienia, może użyć funkcji asynchronicznej i await.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Podsumowanie

Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂

Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie. Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.

Remiksy społeczności