Membuat komponen dialog

Ringkasan dasar tentang cara membangun modal mini dan mega yang adaptif warna, responsif, serta dapat diakses dengan elemen <dialog>.

Dalam postingan ini saya ingin berbagi pendapat saya tentang cara membangun aplikasi yang adaptif warna, modal mini dan mega yang responsif, serta dapat diakses dengan elemen <dialog>. Coba demo dan lihat sumber Google Cloud Anda.

Demonstrasi dialog mega dan mini dalam tema terang dan gelapnya.

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

Ringkasan

Tujuan <dialog> sangat bagus untuk tindakan atau informasi kontekstual dalam halaman. Pertimbangkan kapan pengalaman pengguna dapat memanfaatkan tindakan halaman yang sama, bukan multi-halaman tindakan: mungkin karena formulirnya kecil atau satu-satunya tindakan yang diperlukan dari pengguna adalah konfirmasi atau batal.

Elemen <dialog> baru-baru ini menjadi stabil di seluruh browser:

Dukungan Browser

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Sumber

Saya menemukan bahwa ada beberapa elemen yang hilang, jadi di GUI ini Tantangan Saya menambahkan pengalaman developer item yang saya harapkan: peristiwa tambahan, penutupan cahaya, animasi kustom, dan mini dan mega type.

Markup

Dasar-dasar elemen <dialog> sederhana. Elemen ini akan disembunyikan secara otomatis dan memiliki gaya bawaan untuk menempatkan konten.

<dialog>
  …
</dialog>

Kita dapat meningkatkan garis dasar ini.

Secara tradisional, elemen dialog banyak berbagi dengan modal, dan sering kali bernama dapat dipertukarkan. Saya memilih kebebasan untuk menggunakan elemen dialog untuk pop-up dialog kecil (mini), serta dialog halaman penuh (mega). Saya bernama yaitu mega dan mini, dengan kedua dialog yang sedikit disesuaikan untuk berbagai kasus penggunaan. Saya menambahkan atribut modal-mode agar Anda dapat menentukan jenis:

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

Screenshot dialog mini dan besar dalam tema terang dan gelap.

Tidak selalu, tetapi umumnya elemen dialog akan digunakan untuk mengumpulkan beberapa informasi interaksi. Formulir di dalam elemen dialog dibuat untuk digunakan bersama-sama. Sebaiknya Anda memiliki elemen formulir yang menggabungkan konten dialog sehingga JavaScript dapat mengakses data yang dimasukkan pengguna. Selain itu, tombol di dalam formulir menggunakan method="dialog" dapat menutup dialog tanpa JavaScript dan meneruskan layanan otomatis dan data skalabel.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Dialog besar

Dialog besar memiliki tiga elemen di dalam formulir: <header>, <article>, dan <footer>. Ini berfungsi sebagai kontainer semantik, serta target gaya untuk presentasi dialog. {i>Header<i} memberi judul modal dan menawarkan penutupan tombol. Artikel ini ditujukan untuk informasi dan input formulir. Suatu {i>footer<i} berisi <menu> dari tombol tindakan.

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

Tombol menu pertama memiliki autofocus dan pengendali peristiwa inline onclick. Atribut autofocus akan menerima fokus saat dialog terbuka, dan menurut saya praktik terbaik adalah menempatkan tombol {i>cancel<i}, bukan tombol {i>confirm<i}. Hal ini memastikan bahwa konfirmasi disengaja dan bukan tidak disengaja.

Dialog mini

Dialog mini sangat mirip dengan mega dialog, hanya saja ada Elemen <header>. Ini memungkinkannya menjadi lebih kecil dan lebih inline.

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

Elemen dialog memberikan fondasi kuat untuk elemen area pandang penuh yang dapat mengumpulkan data dan interaksi pengguna. Hal-hal penting ini dapat membuat beberapa orang interaksi yang menarik dan canggih dalam situs atau aplikasi Anda.

Aksesibilitas

Elemen dialog memiliki aksesibilitas bawaan yang sangat baik. Daripada menambahkan teks fitur seperti yang biasa saya lakukan, banyak yang sudah ada.

Memulihkan fokus

Seperti yang kami lakukan secara manual dalam Membangun sebuah navigasi , penting bahwa membuka dan menutup sesuatu dengan benar menempatkan fokus pada pembukaan dan penutupan yang relevan tombol. Ketika navigasi samping tersebut terbuka, fokus ditempatkan pada tombol tutup. Jika ditekan, fokus dikembalikan ke tombol yang membukanya.

Elemen dialog adalah perilaku default bawaan:

Sayangnya, jika Anda ingin menganimasikan dialog masuk dan keluar, fungsi ini hilang. Di bagian JavaScript, saya akan memulihkan fungsionalitasnya.

Menjebak fokus

Elemen dialog mengelola inert untuk Anda pada dokumen. Sebelum inert, JavaScript digunakan untuk memantau fokus meninggalkan elemen, di mana titik itu akan mencegat dan memasukkannya kembali.

Dukungan Browser

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Sumber

Setelah inert, semua bagian dokumen dapat "dibekukan" sehingga mereka tidak lagi berfokus pada target atau interaktif dengan mouse. Alih-alih menjebak fokus, fokus dipandu ke satu-satunya bagian interaktif dari dokumen.

Membuka dan otomatis memfokuskan elemen

Secara default, elemen dialog akan menetapkan fokus ke elemen pertama yang dapat difokuskan di markup dialog. Jika ini bukan elemen terbaik bagi pengguna untuk {i>default<i}, gunakan atribut autofocus. Seperti yang dijelaskan sebelumnya, saya menemukan bahwa praktik terbaik untuk meletakkannya di tombol {i>cancel<i} dan bukan tombol {i>confirm<i}. Hal ini memastikan bahwa konfirmasi tersebut disengaja dan bukan tidak disengaja.

Menutup dengan tombol escape

Sebaiknya, permudah aktivitas ini untuk menutup elemen yang berpotensi mengganggu. Untungnya, elemen dialog akan menangani tombol {i>escape<i}, sehingga Anda dari beban orkestrasi.

Gaya

Ada jalur yang mudah untuk menata gaya elemen dialog dan jalur yang sulit. Mudah jalur dicapai dengan tidak mengubah properti tampilan dialog dan bekerja dengan keterbatasannya. Saya menempuh jalur yang sulit untuk menyediakan animasi kustom bagi membuka dan menutup dialog, mengambil alih properti display dan lainnya.

Menata Gaya dengan Progres Terbuka

Untuk mempercepat warna adaptif dan konsistensi desain secara keseluruhan, saya sangat membawa library variabel CSS Open Props saya. Di beberapa selain variabel yang disediakan gratis, saya juga mengimpor normalisasi file dan beberapa tombol, keduanya adalah Buka Progres disediakan sebagai impor opsional. Impor ini membantu saya berfokus untuk menyesuaikan dialog dan demo tanpa memerlukan banyak gaya untuk mendukungnya dan membuatnya terlihat bagus.

Menata gaya elemen <dialog>

Memiliki properti tampilan

Perilaku tampilkan dan sembunyikan default pada elemen dialog mengubah tampilan block hingga none. Sayangnya, ini berarti video tidak dapat dianimasikan ke dalam dan ke luar, hanya ke dalam. Saya ingin menganimasikan baik ke dalam maupun ke luar, dan langkah pertamanya adalah untuk atur sendiri display:

dialog {
  display: grid;
}

Dengan mengubah, sehingga memiliki, nilai properti tampilan, seperti yang ditunjukkan dalam di atas cuplikan CSS, sejumlah besar gaya perlu dikelola untuk memfasilitasi pengalaman pengguna yang tepat. Pertama, status default dialog adalah tutup. Anda dapat merepresentasikan status ini secara visual dan mencegah dialog menerima interaksi dengan gaya berikut:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

Sekarang dialog tidak terlihat dan tidak dapat berinteraksi jika tidak dibuka. Nanti Saya akan menambahkan beberapa JavaScript untuk mengelola atribut inert pada dialog, untuk memastikan bahwa pengguna {i>keyboard<i} dan pembaca layar juga tidak dapat menjangkau dialog tersembunyi.

Memberikan tema warna adaptif kepada dialog

Dialog besar yang menampilkan tema terang dan gelap, yang menunjukkan warna permukaan.

Sementara color-scheme mengikutsertakan dokumen Anda ke file yang disediakan browser tema warna adaptif dengan preferensi sistem terang dan gelap. Saya ingin menyesuaikan elemen dialog lebih dari itu. Open Props menyediakan beberapa platform warna yang otomatis beradaptasi dengan preferensi sistem terang dan gelap, mirip dengan menggunakan color-scheme. Ini sangat bagus untuk membuat {i>layer <i}dalam desain dan saya suka menggunakan warna mendukung tampilan permukaan lapisan ini secara visual. Warna latar belakangnya adalah var(--surface-1); untuk berada di atas lapisan tersebut, gunakan 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);
  }
}

Warna adaptif lainnya akan ditambahkan nanti untuk elemen turunan, seperti header dan {i>footer<i}. Saya menganggapnya tambahan untuk elemen dialog, tetapi sangat penting dalam membuat desain dialog yang menarik dan dirancang dengan baik.

Ukuran dialog responsif

Secara default, dialog mendelegasikan ukurannya ke kontennya, yang umumnya bagus. Tujuan saya adalah membatasi max-inline-size ke ukuran yang dapat dibaca (--size-content-3 = 60ch) atau 90% dari lebar area tampilan. Ini memastikan dialog tidak akan ditampilkan secara {i>edge to edge<i} di perangkat seluler, dan lebar pada layar {i>desktop<i} sehingga sulit dibaca. Lalu saya menambahkan max-block-size sehingga dialog tidak akan melebihi tinggi halaman. Ini juga berarti bahwa kita akan menentukan di mana area dialog yang dapat di-scroll, jika tinggi elemen dialog.

dialog {
  
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

Perhatikan bahwa saya memiliki max-block-size dua kali? Yang pertama menggunakan 80vh, unit area pandang. Yang benar-benar saya inginkan adalah menjaga dialog dalam alur relatif, untuk pengguna internasional, jadi saya menggunakan mendukung unit dvb dalam deklarasi kedua untuk saat menjadi lebih stabil.

Penempatan dialog mega

Untuk membantu memposisikan elemen dialog, ada baiknya Anda menguraikan dua bagian: tampilan latar layar penuh dan penampung dialog. Tampilan latar harus mencakup semuanya, memberikan efek {i>shadow<i} untuk membantu mendukung bahwa dialog ini di depan dan konten di belakang tidak dapat diakses. Penampung dialog bebas untuk berada di tengahnya dan mengambil bentuk apa pun yang dibutuhkan oleh isinya.

Gaya berikut memperbaiki elemen dialog ke jendela, merentangkannya ke setiap jendela sudut, dan menggunakan margin: auto untuk menempatkan konten di tengah:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Gaya dialog mega seluler

Pada tampilan yang terlihat kecil, saya memberi gaya pada megamodal halaman penuh ini sedikit berbeda. diri setel margin bawah ke 0, yang membawa konten dialog ke bagian bawah area pandang. Dengan beberapa penyesuaian gaya, saya bisa mengubah dialog menjadi {i>actionsheet<i}, lebih dekat ke jempol pengguna:

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

Screenshot devtools overlay spasi margin 
  di dialog mega desktop dan seluler saat terbuka.

Penempatan dialog mini

Ketika menggunakan area pandang yang lebih besar seperti pada komputer desktop, saya memilih untuk memosisikan dialog mini di elemen yang memanggil mereka. Untuk melakukannya, saya membutuhkan JavaScript. Anda dapat menemukan teknik yang saya gunakan di sini, tapi saya merasa itu berada di luar cakupan artikel ini. Tanpa JavaScript, dialog mini akan muncul di tengah layar, seperti dialog mega.

Buat yang menarik

Terakhir, tambahkan gaya ke dialog sehingga terlihat seperti permukaan yang halus di atas halaman. Kelembutan dicapai dengan membulatkan sudut dialog. Kedalaman dicapai dengan salah satu bayangan Open Props yang dibuat dengan cermat properti:

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

Menyesuaikan elemen semu tampilan latar

Saya memilih untuk bekerja sangat ringan dengan latar belakang, hanya menambahkan efek buram dengan backdrop-filter ke dialog mega:

Dukungan Browser

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Sumber

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

Saya juga memilih untuk melakukan transisi pada backdrop-filter, dengan harapan bahwa browser akan memungkinkan transisi elemen tampilan latar di masa mendatang:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Screenshot dialog besar yang menempatkan latar belakang buram avatar berwarna-warni.

Tambahan gaya visual

Saya menyebut bagian ini "tambahan" karena lebih berkaitan dengan elemen dialog demo daripada elemen dialog pada umumnya.

Pembatasan scroll

Saat dialog ditampilkan, pengguna masih dapat menggulir laman di belakangnya, yang tidak saya inginkan:

Biasanya, overscroll-behavior akan menjadi solusi saya yang biasa, tetapi menurut spesifikasi, ini tidak berpengaruh pada dialog karena ini bukan porta gulir, yaitu sebuah {i>scroller<i} sehingga tidak ada yang bisa dicegah. Saya bisa menggunakan JavaScript untuk mengawasi peristiwa baru dari panduan ini, seperti "tertutup" dan "dibuka", lalu tekan tombol overflow: hidden pada dokumen, atau saya bisa menunggu :has() menjadi stabil di semua browser:

Dukungan Browser

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Sumber

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

Sekarang, saat dialog mega terbuka, dokumen html memiliki overflow: hidden.

Tata letak <form>

Selain menjadi elemen yang sangat penting untuk mengumpulkan interaksi informasi dari pengguna, saya menggunakannya di sini untuk meletakkan {i>header<i}, {i>footer<i}, dan elemen artikel. Dengan tata letak ini, saya ingin menyatakan turunan artikel sebagai area yang dapat di-scroll. Saya mencapai ini dengan grid-template-rows Elemen artikel diberi 1fr dan formulir itu sendiri memiliki jumlah maksimum yang sama tinggi sebagai elemen dialog. Menyetel tinggi dan ukuran baris yang tegas ini adalah memungkinkan elemen artikel dibatasi dan men-scroll jika melebihi:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Screenshot devtools yang menempatkan informasi tata letak petak di atas baris.

Menata gaya dialog <header>

Peran elemen ini adalah untuk memberikan judul untuk konten dialog dan tawaran tombol tutup yang mudah ditemukan. Warna ini juga diberi warna permukaan agar tampak seperti berada di belakang konten artikel dialog. Persyaratan ini menghasilkan flexbox penampung, item yang disejajarkan secara vertikal dengan jarak tepi, dan beberapa padding dan celah untuk memberikan ruang pada judul dan tombol tutup:

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

Screenshot Chrome Devtools yang menempatkan informasi tata letak flexbox pada header dialog.

Menata gaya tombol tutup header

Karena demo menggunakan tombol Open Props, tombol tutup disesuaikan menjadi tombol yang berpusat pada ikon bulat seperti:

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

Screenshot Chrome Devtools yang menempatkan informasi ukuran dan padding untuk tombol tutup header.

Menata gaya dialog <article>

Elemen artikel memiliki peran khusus dalam dialog ini: elemen ini adalah ruang yang ditujukan untuk di-scroll jika muncul dialog tinggi atau panjang.

Untuk mencapai hal ini, elemen formulir induk telah menetapkan beberapa batas maksimum untuk yang menjadi pembatas untuk elemen artikel ini jika terlalu tinggi. Setel overflow-y: auto agar scrollbar hanya ditampilkan saat diperlukan, berisi scroll di dalamnya dengan overscroll-behavior: contain, dan sisanya adalah gaya presentasi kustom:

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

Peran {i>footer<i} adalah untuk berisi menu tombol tindakan. Flexbox digunakan untuk ratakan konten ke akhir sumbu sejajar {i>footer<i}, lalu beberapa spasi ke beri ruang pada tombol-tombol tersebut.

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

Screenshot Chrome Devtools yang menempatkan informasi tata letak flexbox pada elemen footer.

menu digunakan untuk memuat tombol tindakan untuk dialog. Model ini menggunakan wrapping tata letak flexbox dengan gap untuk memberikan ruang di antara tombol. Elemen menu memiliki padding seperti <ul>. Saya juga menghapus {i>style<i} itu karena saya tidak membutuhkannya.

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

Screenshot Chrome Devtools yang menempatkan informasi flexbox pada elemen menu footer.

Animasi

Elemen dialog sering dianimasikan karena masuk dan keluar dari jendela. Memberikan dialog beberapa gerakan yang mendukung untuk masuk dan keluar ini membantu pengguna mengarahkan diri mereka dalam alurnya.

Biasanya elemen dialog hanya dapat dianimasikan masuk, bukan keluar. Hal ini karena browser mengalihkan properti display pada elemen. Sebelumnya, panduan atur tampilan ke petak, dan jangan pernah menyetelnya ke tidak ada. Hal ini membuka kemampuan untuk membuat animasi masuk dan keluar.

Propa Terbuka dilengkapi dengan banyak bingkai utama animasi untuk digunakan, sehingga membuat orkestrasi yang mudah dan dibaca. Berikut ini adalah sasaran animasi dan lapisan pendekatan yang saya ambil:

  1. Gerakan yang dikurangi adalah transisi default, opasitas sederhana yang perlahan semakin jelas.
  2. Jika gerakan diperbolehkan, animasi {i>slide<i} dan skala ditambahkan.
  3. Tata letak seluler yang responsif untuk dialog besar disesuaikan agar dapat digeser keluar.

Transisi default yang aman dan bermakna

Meskipun Props Terbuka dilengkapi dengan keyframe untuk memudar masuk dan keluar, saya lebih suka cara ini transisi berlapis sebagai default dengan animasi keyframe sebagai potensi upgrade. Sebelumnya kita sudah menata visibilitas dialog dengan opasitas, mengorkestrasi 1 atau 0 bergantung pada atribut [open]. Kepada transisi antara 0% dan 100%, memberi tahu browser berapa lama dan apa jenis {i>easing <i}yang Anda inginkan:

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

Menambahkan {i>motion <i}pada transisi

Jika pengguna setuju dengan gerakan, baik dialog mega maupun mini harus bergeser sebagai pintu masuk mereka, dan skalakan saat keluar. Anda dapat mencapainya dengan prefers-reduced-motion kueri media dan beberapa Proposisi Terbuka:

@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;
  }
}

Menyesuaikan animasi keluar untuk perangkat seluler

Sebelumnya di bagian gaya visual, gaya dialog besar diadaptasi untuk perangkat seluler perangkat menjadi lebih seperti lembaran aksi, seolah-olah selembar kertas kecil tergelincir naik dari bagian bawah layar dan masih melekat di bagian bawah. Timbangan animasi keluar tidak cocok dengan desain baru ini, dan kita dapat menyesuaikannya dengan beberapa kueri media dan beberapa Open Props:

@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

Ada beberapa hal yang perlu ditambahkan dengan JavaScript:

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

Penambahan ini berasal dari keinginan untuk menutup lampu (mengklik dialog tampilan latar), animasi, dan beberapa peristiwa tambahan untuk pengaturan waktu yang lebih baik data formulir.

Menambahkan tutup terang

Tugas ini mudah dan merupakan tambahan yang bagus untuk elemen dialog yang tidak menjadi animasi. Interaksi dicapai dengan menonton klik pada dialog dan memanfaatkan peristiwa bergelembung untuk menilai apa yang diklik, dan hanya akan close() jika itu adalah elemen paling atas:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Perhatikan dialog.close('dismiss'). Peristiwa dipanggil dan string disediakan. String ini dapat diambil oleh JavaScript lain untuk mendapatkan insight tentang cara dialog ditutup. Anda akan menemukan bahwa saya juga menyediakan {i>close<i} setiap kali saya memanggil fungsi dari berbagai tombol, untuk memberikan konteks ke aplikasi saya tentang interaksi pengguna.

Menambahkan acara penutupan dan acara yang ditutup

Elemen dialog dilengkapi dengan kejadian tutup: elemen itu muncul seketika ketika fungsi dialog close() dipanggil. Karena kita menganimasikan elemen ini, sebaiknya ada peristiwa sebelum dan sesudah animasi, untuk perubahan untuk mengambil atau mengatur ulang bentuk dialog. Saya menggunakannya di sini untuk mengelola penambahan Atribut inert pada dialog tertutup, dan dalam demo saya menggunakannya untuk memodifikasi daftar avatar jika pengguna telah mengirimkan gambar baru.

Untuk melakukannya, buat dua peristiwa baru bernama closing dan closed. Selanjutnya mendengarkan peristiwa tutup bawaan pada dialog. Dari sini, atur dialog ke inert dan mengirim peristiwa closing. Tugas selanjutnya adalah menunggu animasi dan transisi agar selesai berjalan pada dialog, lalu kirimkan Peristiwa 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))

Fungsi animationsComplete, yang juga digunakan dalam bagian Membuat toast , menampilkan promise berdasarkan penyelesaian animasi dan promise transisi. Inilah alasan dialogClose adalah metode asinkron fungsi; model ini kemudian await janji itu kembali dan melangkah maju dengan percaya diri ke acara tertutup.

Menambahkan acara pembukaan dan yang dibuka

Peristiwa ini tidak mudah ditambahkan karena elemen dialog bawaan tidak menyediakan peristiwa terbuka seperti halnya dengan {i>close<i}. Saya menggunakan MutationObserver untuk memberikan insight tentang perubahan atribut dialog. Dalam pengamat ini, Saya akan memantau perubahan pada atribut buka dan mengelola peristiwa kustom sebagaimana mestinya.

Mirip dengan cara kami memulai acara penutupan dan penutupan, buat dua acara baru disebut opening dan opened. Tempat kita sebelumnya memproses dialog tutup , kali ini gunakan pengamat mutasi yang dibuat untuk melihat .


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

Fungsi callback pengamat mutasi akan dipanggil saat dialog atribut diubah, menyediakan daftar perubahan sebagai array. Iterasi ulang atribut berubah, mencari attributeName terbuka. Berikutnya, periksa jika elemen memiliki atribut atau tidak: ini memberi tahu apakah dialog atau tidak telah menjadi terbuka. Jika telah dibuka, hapus atribut inert, setel fokus ke elemen yang meminta autofocus atau elemen button pertama yang ditemukan dalam dialog. Terakhir, mirip dengan penutup dan peristiwa tertutup, segera mengirim peristiwa pembuka, tunggu animasinya untuk menyelesaikan, lalu mengirim peristiwa yang dibuka.

Menambahkan acara yang dihapus

Dalam aplikasi web satu halaman, dialog sering ditambahkan dan dihapus berdasarkan rute atau kebutuhan dan status aplikasi lainnya. Anda dapat membersihkan peristiwa atau data saat dialog dihapus.

Anda dapat melakukannya dengan pengamat mutasi lain. Kali ini, alih-alih mengamati atribut pada elemen dialog, kita akan mengamati turunan bagian dan perhatikan elemen dialog yang dihapus.


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

Callback pengamat mutasi dipanggil setiap kali turunan ditambahkan atau dihapus dari isi dokumen. Mutasi spesifik yang sedang diawasi adalah untuk removedNodes yang memiliki nodeName dari berdialog. Jika dialog dihapus, peristiwa klik dan tutup dihapus untuk mengosongkan memori, dan peristiwa khusus yang dihapus akan dikirim.

Menghapus atribut pemuatan

Untuk mencegah animasi dialog memutar animasi keluar saat ditambahkan ke halaman atau saat pemuatan halaman, atribut pemuatan telah ditambahkan ke dialog. Tujuan skrip berikut menunggu animasi dialog selesai berjalan, lalu menghapus atribut ini. Sekarang dialog bebas untuk dianimasikan masuk dan keluar, dan kita telah secara efektif menyembunyikan animasi yang mengganggu.

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

Pelajari lebih lanjut masalah mencegah animasi keyframe saat pemuatan halaman di sini.

Semuanya

Berikut ini dialog.js secara keseluruhan, setelah kami menjelaskan setiap bagian satu per satu:

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

Menggunakan modul dialog.js

Fungsi yang diekspor dari modul diharapkan untuk dipanggil dan meneruskan dialog elemen yang ingin menambahkan peristiwa dan fungsi baru ini:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Dengan begitu, kedua dialog tersebut akan diupgrade dengan tombol tutup terang, animasi, memuat perbaikan, dan lebih banyak peristiwa untuk dikerjakan.

Memproses peristiwa kustom baru

Setiap elemen dialog yang diupgrade kini dapat memproses lima peristiwa baru, seperti ini:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

Berikut dua contoh penanganan peristiwa tersebut:

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

Dalam demo yang saya buat dengan elemen dialog, saya menggunakan acara tertutup itu dan data formulir untuk menambahkan elemen avatar baru ke dalam daftar. Pemilihan waktu yang tepat adalah bahwa dialog telah menyelesaikan animasi keluarnya, dan kemudian beberapa skrip dianimasikan di avatar baru. Berkat peristiwa baru ini, mengorkestrasi pengalaman pengguna bisa lebih lancar.

Perhatikan dialog.returnValue: ini berisi string tutup yang diteruskan saat peristiwa dialog close() dipanggil. Dalam peristiwa dialogClosed, Anda harus mengetahui apakah dialog telah ditutup, dibatalkan, atau dikonfirmasi. Jika sudah dikonfirmasi, {i>script<i} kemudian mengambil nilai formulir dan mengatur ulang formulir. {i>Reset<i} ini berguna agar ketika dialog ditampilkan lagi, dialog tersebut kosong dan siap untuk pengiriman baru.

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

Resource