Membuat komponen toast

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

Dalam postingan ini, saya ingin membagikan pemikiran tentang cara mem-build komponen toast. Coba 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, animasi ini digunakan sebagai pola masukan antarmuka untuk memberi tahu pengguna tentang hasil tindakan.

Interaksi

Toast tidak seperti notifikasi, peringatan, dan perintah karena tidak interaktif; toast tidak dimaksudkan untuk ditutup atau dipertahankan. Notifikasi ditujukan untuk informasi yang lebih penting, pesan sinkron yang memerlukan interaksi, atau pesan tingkat sistem (bukan tingkat halaman). Toast lebih pasif daripada strategi pemberitahuan lainnya.

Markup

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

Toast

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

Hal ini dapat lebih inklusif dengan menambahkan role="status". Hal ini menyediakan penggantian jika browser tidak memberikan peran implisit sesuai spesifikasi pada elemen <output>.

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

Penampung toast

Lebih dari satu toast dapat ditampilkan sekaligus. Untuk mengatur beberapa toast, penampung digunakan. Penampung ini juga menangani posisi 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 ditambahkan, toast tersebut akan ditumpuk dari tepi layar tersebut.

Penampung GUI

Penampung toast melakukan semua pekerjaan tata letak untuk menampilkan toast. Class ini fixed ke area tampilan dan menggunakan properti logis inset untuk menentukan tepi mana 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 dan padding DevTools yang ditempatkan di elemen .gui-toast-container.

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

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

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

Toast GUI

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

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

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

Gaya

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

Penampung toast

Toast tidak interaktif, mengetuk atau menggesernya tidak akan melakukan apa pun, tetapi saat ini toast menggunakan peristiwa pointer. Cegah toast mencuri klik dengan CSS berikut.

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

Toast GUI

Berikan tema adaptif terang atau gelap pada toast dengan properti kustom, HSL, dan kueri media preferensi.

.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 ditampilkan dengan animasi saat memasuki layar. Menyesuaikan gerakan yang dikurangi dilakukan dengan menetapkan nilai translate ke 0 secara default, tetapi memperbarui nilai gerakan ke durasi dalam kueri media preferensi gerakan . Semua orang mendapatkan beberapa animasi, tetapi hanya beberapa pengguna yang memiliki toast yang berjarak jauh.

Berikut adalah keyframe yang digunakan untuk animasi toast. CSS akan mengontrol masuknya, penantian, 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 mengatur 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 mengelola pembuatan, penambahan, dan penghapusan toast berdasarkan peristiwa pengguna. Pengalaman developer komponen toast harus minimal dan mudah dimulai, seperti ini:

import Toast from './toast.js'

Toast('My first toast')

Membuat grup toast dan toast

Saat modul toast dimuat dari JavaScript, modul tersebut harus membuat penampung toast dan menambahkannya ke halaman. Saya memilih untuk menambahkan elemen sebelum body, hal ini akan membuat masalah penumpukan z-index tidak mungkin terjadi karena penampung berada di atas penampung untuk semua elemen isi.

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

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

Screenshot grup toast di antara tag head dan body.

Fungsi init() dipanggil secara internal ke modul, yang menyimpan elemen sebagai Toaster:

const Toaster = init()

Pembuatan elemen HTML Toast dilakukan dengan fungsi createToast(). Fungsi ini memerlukan beberapa teks untuk toast, membuat elemen <output>, menghiasinya 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 siap menambahkan toast yang dibuat. Fungsi addToast() mengorkestrasi penanganan satu atau beberapa toast. Pertama-tama periksa jumlah toast, dan apakah gerakannya sudah benar, lalu gunakan informasi ini untuk menambahkan toast atau melakukan beberapa animasi menarik sehingga toast lainnya muncul untuk "membuat ruang" bagi 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, animasi keluar. flipToast() dipanggil jika sudah ada toast yang menggunakan teknik yang disebut FLIP oleh Paul Lewis. Idenya adalah menghitung perbedaan posisi penampung, sebelum dan sesudah toast baru ditambahkan. Anggaplah seperti menandai lokasi Toaster saat ini, lokasinya nanti, lalu mengoanimasi dari lokasi sebelumnya ke lokasinya sekarang.

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 pengangkatan tata letak. Saat toast baru ditambahkan, petak akan menempatkannya di awal dan memberi spasi dengan yang lain. Sementara itu, animasi web digunakan untuk menganimasikan penampung dari posisi lama.

Menggabungkan semua JavaScript

Saat Toast('my first toast') dipanggil, toast akan dibuat, ditambahkan ke halaman (bahkan mungkin container dianimasikan untuk mengakomodasi toast baru), promise akan ditampilkan dan toast yang dibuat ditonton 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 ada dalam fungsi Promise.allSettled() dan pemetaan toast.getAnimations(). Karena saya menggunakan beberapa animasi keyframe untuk toast, agar dapat mengetahui dengan yakin bahwa semuanya telah selesai, setiap animasi harus diminta dari JavaScript dan setiap promise finished diamati untuk diselesaikan. allSettled melakukan hal itu untuk kita, menyelesaikannya sendiri setelah semua promise-nya telah terpenuhi. Menggunakan await Promise.allSettled() berarti baris kode berikutnya dapat menghapus elemen tanpa ragu dan menganggap toast telah menyelesaikan siklus prosesnya. 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, agar skrip lain dapat diimpor dan digunakan.

Menggunakan komponen Toast

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

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

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

import Toast from './toast.js'

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

Kesimpulan

Setelah Anda tahu cara saya melakukannya, bagaimana Anda melakukannya‽ 🙂

Mari kita diversifikasi pendekatan dan pelajari semua cara untuk mem-build di web. Buat demo, tweet link-nya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas