Membuat komponen toast

Ringkasan dasar tentang cara membangun komponen toast yang adaptif dan mudah diakses.

Dalam postingan ini saya ingin berbagi pemikiran tentang cara membangun komponen toast. Mulai demo.

Demo

Jika Anda lebih suka menonton video, berikut versi YouTube untuk postingan ini:

Ringkasan

Toast adalah pesan singkat non-interaktif, pasif, dan asinkron untuk pengguna. Umumnya mereka digunakan sebagai pola umpan balik antarmuka untuk menginformasikan pengguna tentang hasil dari suatu tindakan.

Interaksi

Toast tidak seperti notifikasi, pemberitahuan dan perintah karena mereka tidak interaktif; laporan itu tidak dimaksudkan untuk diberhentikan atau dipertahankan. Notifikasi adalah untuk informasi yang lebih penting, pesan sinkron yang memerlukan interaksi, atau pesan tingkat sistem (bukan pesan tingkat halaman). Toast lebih pasif daripada strategi pemberitahuan lainnya.

Markup

Tujuan <output> adalah pilihan yang baik untuk toast karena diumumkan ke layar pembaca. HTML yang benar memberikan dasar yang aman bagi kita untuk menyempurnakan JavaScript dan CSS, dan akan ada banyak JavaScript.

Bersulang

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

Dapat lebih inklusif dengan menambahkan role="status". Hal ini memberikan fallback jika browser tidak memberi <output> elemen implisit peran sesuai spesifikasi.

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

Penampung toast

Lebih dari satu toast dapat ditampilkan sekaligus. Untuk mengorkestrasi beberapa {i>toast<i}, sebuah kontainer digunakan. Penampung ini juga menangani posisi memunculkan toast di layar.

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

Tata letak

Saya memilih untuk menyematkan toast ke inset-block-end area pandang, dan jika lebih banyak toast yang ditambahkan, toast akan ditumpuk dari tepi layar tersebut.

Penampung GUI

Penampung toast melakukan semua tugas tata letak untuk menyajikan toast. Penting fixed ke area pandang dan menggunakan properti logis inset untuk menentukan tepi yang akan disematkan, serta sedikit padding dari tepi block-end yang sama.

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

Screenshot dengan ukuran kotak DevTools dan padding yang ditempatkan pada elemen .gui-toast-container.

Selain memposisikan dirinya sendiri dalam area pandang, penampung toast adalah yang dapat meratakan dan mendistribusikan toast. Item dipusatkan sebagai kelompokkan dengan justify-content dan dipusatkan satu per satu dengan justify-items. Tambahkan sedikit gap agar toast tidak menyentuh.

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

Screenshot berisi overlay petak CSS pada grup toast, kali ini
menyoroti ruang dan celah di antara elemen turunan toast.

Roti GUI

Setiap toast memiliki padding, beberapa sudut lebih lembut dengan border-radius, dan fungsi min() untuk membantu dalam hal ukuran seluler dan desktop. Ukuran responsif di CSS berikut mencegah toast tumbuh lebih lebar dari 90% dari area pandang atau 25ch

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

Screenshot elemen .gui-toast tunggal, dengan padding dan batas
radius ditampilkan.

Gaya

Setelah tata letak dan penempatan disetel, tambahkan CSS yang membantu beradaptasi dengan pengguna setelan dan interaksi pengguna.

Penampung toast

Toast tidak interaktif, mengetuk atau menggesernya tidak akan melakukan apa pun, tapi mereka saat ini mengonsumsi peristiwa pointer. Mencegah toast mencuri klik dengan CSS berikut.

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

Roti GUI

Memberi toast tema adaptif terang atau gelap dengan properti kustom, HSL, dan preferensi media.

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

Animasi

Toast baru akan muncul dengan animasi saat memasuki layar. Mengakomodasi gerakan yang dikurangi dilakukan dengan menetapkan nilai translate ke 0 dengan default, tetapi memperbarui nilai gerakan dengan panjang di media preferensi gerakan . Semua orang mendapatkan animasi, tetapi hanya beberapa pengguna yang melakukan perjalanan toast berjauhan.

Berikut adalah keyframe yang digunakan untuk animasi toast. CSS akan mengontrol pintu masuk, waktu tunggu, dan keluarnya toast, semuanya dalam satu animasi.

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

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

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

Elemen toast kemudian menyiapkan variabel dan mengorkestrasi keyframe.

.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

Dengan gaya dan HTML yang dapat diakses pembaca layar, JavaScript diperlukan untuk mengatur pembuatan, penambahan, dan penghancuran toast berdasarkan peristiwa. Pengalaman developer komponen toast harus minimal dan mudah untuk memulainya, seperti ini:

import Toast from './toast.js'

Toast('My first toast')

Membuat grup toast dan toast

Saat modul toast dimuat dari JavaScript, modul toast harus membuat penampung toast dan menambahkannya ke laman. Saya memilih untuk menambahkan elemen sebelum body, ini akan membuat Masalah penumpukan z-index mungkin tidak terjadi karena penampung di atas container untuk semua elemen {i>body<i}.

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

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

Screenshot grup toast antara tag head dan body.

Fungsi init() dipanggil secara internal ke modul, sehingga menyembunyikan elemen tersebut sebagai Toaster:

const Toaster = init()

Pembuatan elemen HTML toast selesai dengan fungsi createToast(). Tujuan fungsi memerlukan beberapa teks untuk toast, membuat elemen <output>, menghiasi dengan beberapa class dan atribut, menetapkan teks, dan menampilkan node.

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

  return node
}

Mengelola satu atau beberapa toast

JavaScript kini menambahkan penampung ke dokumen untuk berisi toast dan merupakan siap menambahkan toast yang dibuat. Fungsi addToast() mengorkestrasi penanganan satu atau banyak toast. Pertama-tama, periksa jumlah toast, dan apakah {i>motion<i} baik, menggunakan informasi ini untuk menambahkan toast atau membuat animasi sehingga toast lainnya akan muncul untuk "mengosongkan ruang" untuk toast baru.

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

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

Saat menambahkan toast pertama, Toaster.appendChild(toast) menambahkan toast ke halaman yang memicu animasi CSS: animasi masuk, tunggu 3s, animasikan keluar. flipToast() dipanggil saat toast yang ada sudah ada, menggunakan teknik disebut FLIP oleh Paul Leo. Idenya adalah untuk menghitung perbedaan di posisi penampung, sebelum dan sesudah toast baru ditambahkan. Anggap saja seperti menandai di mana Pemanggang Roti sekarang, di mana letaknya, maka membuat animasi dari tempatnya ke tempatnya.

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

Petak CSS melakukan penghapusan tata letak. Saat toast baru ditambahkan, petak akan menempatkannya di awal dan menempatkannya dengan yang lain. Sementara itu, laman animasi adalah yang digunakan untuk menganimasikan container dari posisi lama.

Menggabungkan semua JavaScript

Saat Toast('my first toast') dipanggil, toast akan dibuat dan ditambahkan ke halaman (bahkan mungkin kontainernya dianimasikan untuk mengakomodasi toast baru), janji ditampilkan dan toast yang dibuat akan telah menonton untuk Penyelesaian animasi CSS (tiga animasi keyframe) untuk resolusi promise.

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

Saya merasa bagian yang membingungkan dari kode ini adalah dalam fungsi Promise.allSettled() dan toast.getAnimations(). Karena saya menggunakan animasi beberapa keyframe untuk roti panggang, agar dengan yakin mengetahui semuanya telah selesai, masing-masing harus yang diminta dari JavaScript dan setiap finished yang dijanjikan untuk diselesaikan. allSettled apakah itu berhasil untuk kita, menyelesaikan dirinya sendiri setelah semua janjinya telah terpenuhi. Menggunakan await Promise.allSettled() berarti baris berikutnya dari kode dapat menghapus elemen tanpa ragu dan menganggap toast telah menyelesaikan siklus proses. Terakhir, memanggil resolve() akan memenuhi promise Toast tingkat tinggi sehingga developer dapat membersihkan atau melakukan pekerjaan lain setelah toast ditampilkan.

export default Toast

Terakhir, fungsi Toast diekspor dari modul, untuk skrip lain ke impor dan gunakan.

Menggunakan komponen Toast

Penggunaan toast, atau pengalaman developer toast, dilakukan dengan mengimpor Toast dan memanggilnya dengan string pesan.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Jika developer ingin melakukan pembersihan atau apa pun, setelah toast ditampilkan, mereka dapat menggunakan await.

import Toast from './toast.js'

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

Kesimpulan

Sekarang setelah Anda tahu bagaimana saya melakukannya, bagaimana Anda akan 🙂

Mari kita diversifikasi pendekatan kami dan mempelajari semua cara untuk membangun di web. Buat demo, link tweet saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah ini.

Remix komunitas