Membuat komponen toast

Ringkasan dasar tentang cara membuat komponen toast adaptif dan aksesibel.

Dalam postingan ini, saya ingin berbagi pemikiran tentang cara membuat komponen toast. Coba demo.

Demo

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

Ringkasan

Toast adalah pesan singkat yang tidak interaktif, pasif, dan asinkron untuk pengguna. Umumnya, pola ini digunakan sebagai pola masukan antarmuka untuk memberi tahu pengguna tentang hasil suatu tindakan.

Interaksi

Toast tidak seperti notifikasi, peringatan dan perintah karena tidak interaktif; tidak dimaksudkan untuk ditutup atau tetap ada. 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 ke pembaca layar. HTML yang benar memberikan dasar yang aman bagi kita untuk ditingkatkan dengan JavaScript dan CSS, dan akan ada banyak JavaScript.

Bersulang

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

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

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

Penampung toast

Lebih dari satu toast dapat ditampilkan sekaligus. Untuk mengatur beberapa toast, digunakan sebuah penampung. 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 viewport, dan jika lebih banyak toast ditambahkan, toast akan ditumpuk dari tepi layar tersebut.

Penampung GUI

Penampung toast melakukan semua pekerjaan tata letak untuk menampilkan toast. Elemen ini fixed ke area tampilan dan menggunakan properti logis inset untuk menentukan tepi yang akan disematkan, ditambah 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 ditumpuk pada elemen .gui-toast-container.

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

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

Screenshot dengan overlay petak CSS pada grup toast, kali ini
menyoroti 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 penentuan ukuran seluler dan desktop. Ukuran responsif dalam CSS berikut mencegah toast tumbuh lebih lebar dari 90% viewport 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 ditampilkan.

Gaya

Setelah tata letak dan pemosisian ditetapkan, tambahkan CSS yang membantu beradaptasi dengan setelan dan interaksi pengguna.

Penampung toast

Toast tidak interaktif, mengetuk atau menggeser toast 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 muncul dengan animasi saat memasuki layar. Mengakomodasi gerakan yang dikurangi dilakukan dengan menyetel nilai translate ke 0 secara default, tetapi memperbarui nilai gerakan ke panjang dalam kueri media preferensi gerakan . Semua orang mendapatkan beberapa animasi, tetapi hanya beberapa pengguna yang memiliki perjalanan toast sejauh jarak tertentu.

Berikut adalah keyframe yang digunakan untuk animasi toast. CSS akan mengontrol masuk, menunggu, dan keluar 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 siap, JavaScript diperlukan untuk mengatur pembuatan, penambahan, dan penghapusan toast berdasarkan peristiwa pengguna. Pengalaman developer komponen toast harus minimal dan mudah untuk memulai, 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, 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>, menghiasnya 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 banyak toast

JavaScript kini menambahkan penampung ke dokumen untuk memuat toast dan siap menambahkan toast yang dibuat. Fungsi addToast() mengatur penanganan satu atau banyak toast. Pertama, periksa jumlah toast dan apakah gerakan sudah sesuai, lalu gunakan informasi ini untuk menambahkan toast atau melakukan beberapa animasi menarik sehingga toast lain tampak "memberi 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: masuk, tunggu 3s, keluar. flipToast() dipanggil saat ada toast yang sudah ada, menggunakan teknik yang disebut FLIP oleh Paul Lewis. Idenya adalah menghitung perbedaan posisi penampung, sebelum dan sesudah toast baru ditambahkan. Anggap saja seperti menandai lokasi Toaster saat ini, lokasi tujuannya, lalu menganimasikan dari lokasi awalnya ke lokasi saat ini.

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 mengatur jaraknya dengan toast lainnya. Sementara itu, animasi web digunakan untuk menganimasikan penampung dari posisi lama.

Menggabungkan semua JavaScript

Saat Toast('my first toast') dipanggil, toast dibuat, ditambahkan ke halaman (bahkan mungkin penampung dianimasikan untuk mengakomodasi toast baru), promise dikembalikan dan toast yang dibuat dipantau untuk penyelesaian animasi CSS (tiga animasi keyframe) untuk penyelesaian 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 di fungsi Promise.allSettled() dan pemetaan toast.getAnimations(). Karena saya menggunakan beberapa animasi keyframe untuk toast, agar yakin bahwa semuanya telah selesai, setiap animasi harus diminta dari JavaScript dan setiap finished promise-nya diamati hingga selesai. allSettled berfungsi untuk kita, dan akan diselesaikan setelah semua janjinya terpenuhi. Menggunakan await Promise.allSettled() berarti baris kode berikutnya dapat menghapus elemen dengan yakin dan mengasumsikan bahwa toast telah menyelesaikan siklus prosesnya. Terakhir, panggilan resolve() memenuhi janji 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 mengimpor dan menggunakannya.

Menggunakan komponen Toast

Penggunaan 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 async dan 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 cara saya melakukannya, bagaimana Anda‽ 🙂

Mari kita diversifikasi pendekatan kita dan pelajari semua cara untuk membangun di web. Buat demo, tweet linknya kepada saya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas