Membuat komponen dialog

Ringkasan dasar tentang cara membuat modal mini dan mega yang adaptif terhadap warna, responsif, dan mudah diakses dengan elemen <dialog>.

Dalam postingan ini, saya ingin membagikan pemikiran saya tentang cara membuat modal mini dan mega yang adaptif terhadap warna, responsif, dan mudah diakses dengan elemen <dialog>. Coba demo dan lihat sumbernya.

Demonstrasi dialog mega dan mini dalam tema terang dan gelap.

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

Ringkasan

Elemen <dialog> sangat cocok untuk informasi atau tindakan kontekstual dalam halaman. Pertimbangkan kapan pengalaman pengguna dapat memanfaatkan tindakan halaman yang sama, bukan tindakan multi-halaman: mungkin karena formulir berukuran kecil atau satu-satunya tindakan yang diperlukan dari pengguna adalah mengonfirmasi atau membatalkan.

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

Dukungan Browser

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

Sumber

Saya mendapati bahwa elemen tersebut tidak memiliki beberapa hal, jadi dalam Tantangan GUI ini, saya menambahkan item pengalaman developer yang saya harapkan: peristiwa tambahan, penutupan ringan, animasi kustom, serta jenis mini dan mega.

Markup

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

<dialog>
  …
</dialog>

Kita dapat meningkatkan dasar pengukuran ini.

Biasanya, elemen dialog sering berbagi dengan modal, dan sering kali namanya dapat dipertukarkan. Di sini saya bebas menggunakan elemen dialog untuk pop-up dialog kecil (mini), serta dialog halaman penuh (mega). Saya menamainya mega dan mini, dan kedua dialog itu sedikit disesuaikan untuk kasus penggunaan yang berbeda. 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 bersama-sama. Sebaiknya sediakan elemen formulir yang menggabungkan konten dialog Anda sehingga JavaScript dapat mengakses data yang telah dimasukkan pengguna. Selain itu, tombol di dalam formulir yang menggunakan method="dialog" dapat menutup dialog tanpa JavaScript dan meneruskan data.

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

Dialog mega

Dialog mega memiliki tiga elemen di dalam formulir: <header>, <article>, dan <footer>. Ini berfungsi sebagai penampung semantik, serta target gaya untuk presentasi dialog. Header memberi judul pada modal dan menawarkan tombol tutup. Artikel ini ditujukan untuk input dan informasi formulir. Footer menyimpan <menu> 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 dibuka, dan saya merasa praktik terbaiknya adalah menempatkannya di tombol batal, bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan secara sengaja dan bukan secara tidak sengaja.

Dialog mini

Dialog mini sangat mirip dengan dialog mega, hanya saja tidak memiliki elemen <header>. Hal 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 yang kuat untuk elemen area pandang penuh yang dapat mengumpulkan data dan interaksi pengguna. Dasar-dasar ini dapat menghasilkan beberapa interaksi yang sangat menarik dan efektif di situs atau aplikasi Anda.

Aksesibilitas

Elemen dialog memiliki aksesibilitas bawaan yang sangat baik. Alih-alih menambahkan fitur ini seperti yang biasa saya lakukan, banyak fitur yang sudah ada.

Memulihkan fokus

Seperti yang kita lakukan secara manual di Mem-build komponen samping, penting untuk membuka dan menutup sesuatu dengan benar agar fokus pada tombol buka dan tutup yang relevan. Saat navigasi samping tersebut terbuka, fokus ditempatkan pada tombol tutup. Saat tombol tutup ditekan, fokus akan dipulihkan ke tombol yang membukanya.

Dengan elemen dialog, ini adalah perilaku default bawaan:

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

Memblokir fokus

Elemen dialog mengelola inert untuk Anda di dokumen. Sebelum inert, JavaScript digunakan untuk memantau fokus yang keluar dari elemen, pada saat itu JavaScript akan menangkap dan mengembalikannya.

Dukungan Browser

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

Sumber

Setelah inert, setiap bagian dokumen dapat "dibekukan" sehingga tidak lagi menjadi target fokus atau interaktif dengan mouse. Fokus tidak terperangkap, tetapi diarahkan ke satu-satunya bagian interaktif dalam dokumen.

Membuka dan memfokuskan elemen secara otomatis

Secara default, elemen dialog akan menetapkan fokus ke elemen pertama yang dapat difokuskan dalam markup dialog. Jika ini bukan elemen terbaik untuk digunakan pengguna secara default, gunakan atribut autofocus. Seperti yang dijelaskan sebelumnya, menurut saya praktik terbaiknya adalah menempatkan pesan ini di tombol batal, bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan secara sengaja dan bukan secara tidak sengaja.

Menutup dengan tombol escape

Penting untuk memudahkan penutupan elemen yang berpotensi mengganggu ini. Untungnya, elemen dialog akan menangani tombol escape untuk Anda, sehingga Anda tidak perlu memikirkan beban orkestrasi.

Gaya

Ada jalur mudah untuk menata gaya elemen dialog dan jalur sulit. Jalur mudah dicapai dengan tidak mengubah properti tampilan dialog dan mengatasi batasannya. Saya memilih jalur yang sulit untuk memberikan animasi kustom untuk 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 dengan tidak malu-malu membawa library variabel CSS Open Props. Selain variabel gratis yang disediakan, saya juga mengimpor file normalize dan beberapa tombol, yang keduanya disediakan oleh Open Props sebagai impor opsional. Impor ini membantu saya berfokus pada penyesuaian dialog dan demo tanpa memerlukan banyak gaya untuk mendukungnya dan membuatnya terlihat bagus.

Menata gaya elemen <dialog>

Memiliki properti tampilan

Perilaku default tampilkan dan sembunyikan elemen dialog akan mengalihkan properti tampilan dari block ke none. Sayangnya, ini berarti tidak dapat dianimasikan masuk dan keluar, hanya masuk. Saya ingin menganimasikan baik ke dalam maupun ke luar, dan langkah pertama adalah menetapkan properti display saya sendiri:

dialog {
  display: grid;
}

Dengan mengubah, dan karenanya memiliki, nilai properti tampilan, seperti yang ditunjukkan dalam cuplikan CSS di atas, sejumlah besar gaya perlu dikelola untuk memfasilitasi pengalaman pengguna yang tepat. Pertama, status default dialog ditutup. 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 saat tidak terbuka. Nanti, saya akan menambahkan beberapa JavaScript untuk mengelola atribut inert pada dialog, yang memastikan bahwa pengguna keyboard dan pembaca layar juga tidak dapat menjangkau dialog tersembunyi.

Memberikan tema warna adaptif pada dialog

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

Meskipun color-scheme mengikutsertakan dokumen Anda ke tema warna adaptif yang disediakan browser terhadap preferensi sistem terang dan gelap, saya ingin lebih menyesuaikan elemen dialog dari itu. Progres Terbuka menyediakan beberapa warna permukaan yang otomatis beradaptasi dengan preferensi sistem terang dan gelap, mirip dengan menggunakan color-scheme. Hal ini sangat bagus untuk membuat lapisan dalam desain dan saya suka menggunakan warna untuk membantu 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);
  }
}

Lebih banyak warna adaptif akan ditambahkan nanti untuk elemen turunan, seperti header dan footer. Saya menganggapnya sebagai tambahan untuk elemen dialog, tetapi sangat penting dalam membuat desain dialog yang menarik dan dirancang dengan baik.

Ukuran dialog responsif

Dialog secara default mendelegasikan ukurannya ke kontennya, yang umumnya sangat bagus. Tujuan saya di sini adalah membatasi max-inline-size ke ukuran yang dapat dibaca (--size-content-3 = 60ch) atau 90% lebar area pandang. Hal ini memastikan dialog tidak akan memenuhi seluruh layar di perangkat seluler, dan tidak akan terlalu lebar di layar desktop sehingga sulit dibaca. Kemudian, saya menambahkan max-block-size sehingga dialog tidak akan melebihi tinggi halaman. Ini juga berarti bahwa kita harus menentukan lokasi area dialog yang dapat di-scroll, jika elemen dialognya tinggi.

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 fisik. Yang saya inginkan adalah mempertahankan dialog dalam alur relatif, untuk pengguna internasional, jadi saya menggunakan unit dvb yang logis, lebih baru, dan hanya didukung sebagian dalam deklarasi kedua saat menjadi lebih stabil.

Pemosisi dialog Mega

Untuk membantu memosisikan elemen dialog, sebaiknya bagi dua bagiannya: latar belakang layar penuh dan penampung dialog. Latar belakang harus menutupi semuanya, memberikan efek bayangan untuk membantu mendukung bahwa dialog ini ada di depan dan konten di belakang tidak dapat diakses. Penampung dialog bebas untuk memusatkan dirinya di atas latar belakang ini dan mengambil bentuk apa pun yang diperlukan kontennya.

Gaya berikut memperbaiki elemen dialog ke jendela, merentangkan ke setiap sudut, dan menggunakan margin: auto untuk memusatkan konten:

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

Pada area pandang kecil, saya menata gaya mega modal halaman penuh ini dengan sedikit berbeda. Saya menyetel margin bawah ke 0, yang membawa konten dialog ke bagian bawah area tampilan. Dengan beberapa penyesuaian gaya, saya dapat mengubah dialog menjadi actionsheet, lebih dekat dengan ibu jari 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 yang menempatkan spasi margin di dialog mega desktop dan seluler saat terbuka.

Pemosisi dialog mini

Saat menggunakan area pandang yang lebih besar seperti di komputer desktop, saya memilih untuk memosisikan dialog mini di atas elemen yang memanggilnya. Untuk melakukannya, saya memerlukan JavaScript. Anda dapat menemukan teknik yang saya gunakan di sini, tetapi kami rasa hal ini di luar cakupan artikel ini. Tanpa JavaScript, dialog mini akan muncul di tengah layar, seperti dialog mega.

Buat atraktif

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

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

Menyesuaikan elemen semu tampilan latar

Saya memilih untuk bekerja dengan sangat ringan dengan latar belakang, hanya menambahkan efek blur 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 menempatkan transisi pada backdrop-filter, dengan harapan browser akan mengizinkan transisi elemen latar belakang di masa mendatang:

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

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

Tambahan gaya visual

Saya menyebut bagian ini "tambahan" karena lebih berkaitan dengan demo elemen dialog saya daripada elemen dialog secara umum.

Pembatasan scroll

Saat dialog ditampilkan, pengguna masih dapat men-scroll halaman di belakangnya, yang tidak saya inginkan:

Biasanya, overscroll-behavior akan menjadi solusi saya yang biasa, tetapi sesuai dengan spesifikasi, hal ini tidak berpengaruh pada dialog karena bukan port scroll, yaitu bukan penggeser sehingga tidak ada yang perlu dicegah. Saya dapat menggunakan JavaScript untuk memproses peristiwa baru dari panduan ini, seperti "closed" dan "opened", serta mengalihkan overflow: hidden di dokumen, atau saya dapat menunggu :has() 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 informasi interaksi dari pengguna, saya menggunakannya di sini untuk menata elemen header, footer, dan artikel. Dengan tata letak ini, saya ingin menetapkan turunan artikel sebagai area yang dapat di-scroll. Saya mencapainya dengan grid-template-rows. Elemen artikel diberi 1fr dan formulir itu sendiri memiliki tinggi maksimum yang sama dengan elemen dialog. Menetapkan tinggi dan ukuran baris yang tetap ini adalah hal yang memungkinkan elemen artikel dibatasi dan di-scroll saat melampaui batas:

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 memberikan judul untuk konten dialog dan menawarkan tombol tutup yang mudah ditemukan. Ikon ini juga diberi warna permukaan agar terlihat seperti berada di belakang konten artikel dialog. Persyaratan ini menyebabkan penampung flexbox, item yang disejajarkan secara vertikal dengan jarak ke tepinya, 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 di header dialog.

Menata gaya tombol tutup header

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

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: ini adalah ruang yang dimaksudkan untuk di-scroll jika dialog tinggi atau panjang.

Untuk mencapai hal ini, elemen formulir induk telah menetapkan beberapa batas maksimum untuk dirinya sendiri yang memberikan batasan untuk elemen artikel ini jika terlalu tinggi. Setel overflow-y: auto sehingga scrollbar hanya ditampilkan saat diperlukan, berisi scroll di dalamnya dengan overscroll-behavior: contain, dan sisanya akan berupa 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 footer adalah berisi menu tombol tindakan. Flexbox digunakan untuk menyesuaikan konten ke akhir sumbu inline footer, lalu beberapa spasi untuk memberi tombol ruang.

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.

Elemen menu digunakan untuk memuat tombol tindakan untuk dialog. Fungsi ini menggunakan tata letak flexbox pembungkus dengan gap untuk menyediakan ruang di antara tombol. Elemen menu memiliki padding seperti <ul>. Saya juga menghapus gaya tersebut karena tidak diperlukan.

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 di elemen menu footer.

Animasi

Elemen dialog sering kali dianimasikan karena masuk dan keluar jendela. Memberikan dialog beberapa gerakan pendukung untuk pintu masuk dan keluar ini membantu pengguna melakukan orientasi dalam alur.

Biasanya, elemen dialog hanya dapat dianimasikan masuk, bukan keluar. Hal ini karena browser mengalihkan properti display pada elemen. Sebelumnya, panduan menetapkan tampilan ke petak, dan tidak pernah menetapkannya ke tidak ada. Ini membuka kemampuan untuk menganimasikan masuk dan keluarnya video.

Open Props dilengkapi dengan banyak animasi keyframe untuk digunakan, yang membuat orkestrasi menjadi mudah dan mudah dibaca. Berikut adalah sasaran animasi dan pendekatan berlapis yang saya lakukan:

  1. Pengurangan gerakan adalah transisi default, yaitu opasitas sederhana yang memudar dan muncul.
  2. Jika gerakannya sudah baik, animasi geser dan skala akan ditambahkan.
  3. Tata letak seluler responsif untuk dialog mega disesuaikan agar dapat di-slide keluar.

Transisi default yang aman dan bermakna

Meskipun Open Props dilengkapi dengan keyframe untuk memudar dan menghilang, saya lebih memilih pendekatan transisi berlapis ini sebagai default dengan animasi keyframe sebagai potensi upgrade. Sebelumnya, kita sudah menata gaya visibilitas dialog dengan opasitas, mengorkestrasi 1 atau 0, bergantung pada atribut [open]. Untuk bertransisi antara 0% dan 100%, beri tahu browser berapa lama dan jenis easing yang Anda inginkan:

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

Menambahkan gerakan ke transisi

Jika pengguna tidak keberatan dengan gerakan, dialog mega dan mini akan bergeser ke atas sebagai pintu masuk, dan diskalakan ke luar sebagai pintu keluar. Anda dapat melakukannya dengan kueri media prefers-reduced-motion 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 mega disesuaikan untuk perangkat seluler agar lebih mirip dengan sheet tindakan, seolah-olah selembar kertas kecil telah bergeser ke atas dari bagian bawah layar dan masih menempel di bagian bawah. Animasi keluar penyebaran skala tidak cocok dengan desain baru ini, dan kita dapat menyesuaikannya dengan beberapa kueri media dan beberapa Proper Terbuka:

@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 ringan (mengklik latar belakang dialog), animasi, dan beberapa peristiwa tambahan untuk mendapatkan waktu yang lebih baik dalam mendapatkan data formulir.

Menambahkan tutup terang

Tugas ini mudah dilakukan dan merupakan tambahan yang bagus untuk elemen dialog yang tidak dianimasikan. Interaksi dicapai dengan menonton klik pada elemen dialog dan memanfaatkan peristiwa bubble 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 diberikan. String ini dapat diambil oleh JavaScript lain untuk mendapatkan insight tentang cara dialog ditutup. Saya juga telah menyediakan string close setiap kali memanggil fungsi dari berbagai tombol, untuk memberikan konteks ke aplikasi saya tentang interaksi pengguna.

Menambahkan peristiwa penutupan dan tertutup

Elemen dialog disertai peristiwa tutup: peristiwa tersebut langsung muncul saat fungsi close() dialog dipanggil. Karena kita menganimasikan elemen ini, sebaiknya ada peristiwa sebelum dan sesudah animasi, untuk perubahan guna mengambil data atau mereset formulir dialog. Saya menggunakannya di sini untuk mengelola penambahan atribut inert pada dialog tertutup, dan dalam demo, saya menggunakannya untuk mengubah daftar avatar jika pengguna telah mengirimkan gambar baru.

Untuk melakukannya, buat dua peristiwa baru bernama closing dan closed. Kemudian, simak peristiwa tutup bawaan di dialog. Dari sini, tetapkan dialog ke inert dan kirim peristiwa closing. Tugas berikutnya adalah menunggu animasi dan transisi selesai berjalan di dialog, lalu mengirim 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 Mem-build komponen toast, menampilkan promise berdasarkan penyelesaian animasi dan promise transisi. Inilah alasan mengapa dialogClose adalah fungsi asinkron; kemudian dapat await janji yang ditampilkan dan bergerak maju tanpa ragu ke peristiwa tertutup.

Menambahkan peristiwa pembukaan dan dibuka

Peristiwa ini tidak mudah ditambahkan karena elemen dialog bawaan tidak menyediakan peristiwa terbuka seperti halnya dengan peristiwa tutup. Saya menggunakan MutationObserver untuk memberikan insight tentang perubahan atribut dialog. Dalam observer ini, saya akan mengamati perubahan pada atribut buka dan mengelola peristiwa kustom sebagaimana mestinya.

Serupa dengan cara kita memulai peristiwa penutupan dan ditutup, buat dua peristiwa baru yang disebut opening dan opened. Jika sebelumnya kita memproses peristiwa penutupan dialog, kali ini gunakan observer mutasi yang dibuat untuk melihat atribut dialog.


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 atribut dialog diubah, yang memberikan daftar perubahan sebagai array. Lakukan iterasi pada perubahan atribut, cari attributeName yang terbuka. Selanjutnya, periksa apakah elemen memiliki atribut atau tidak: hal ini akan memberi tahu apakah dialog terbuka atau tidak. Jika telah dibuka, hapus atribut inert, tetapkan fokus ke elemen yang meminta autofocus atau elemen button pertama yang ditemukan dalam dialog. Terakhir, mirip dengan peristiwa penutupan dan tertutup, langsung mengirim peristiwa pembuka, tunggu hingga animasi selesai, lalu kirim peristiwa yang dibuka.

Menambahkan peristiwa yang dihapus

Dalam aplikasi web satu halaman, dialog sering ditambahkan dan dihapus berdasarkan rute atau kebutuhan dan status aplikasi lainnya. Sebaiknya bersihkan 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 elemen isi dan mengamati 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 dipantau adalah untuk removedNodes yang memiliki nodeName dialog. Jika dialog dihapus, peristiwa klik dan tutup dihapus untuk mengosongkan memori, dan peristiwa kustom yang dihapus akan dikirim.

Menghapus atribut pemuatan

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

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 adalah dialog.js secara keseluruhan, setelah kita menjelaskan setiap bagian secara terpisah:

// 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 akan dipanggil dan meneruskan elemen dialog 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 diupgrade dengan penutupan ringan, perbaikan pemuatan animasi, dan lebih banyak peristiwa yang dapat digunakan.

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 adalah 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 peristiwa tertutup tersebut dan data formulir untuk menambahkan elemen avatar baru ke daftar. Waktunya tepat karena dialog telah menyelesaikan animasi keluarnya, lalu beberapa skrip menganimasikan avatar baru. Berkat peristiwa baru, pengaturan pengalaman pengguna dapat menjadi lebih lancar.

Pemberitahuan dialog.returnValue: file ini berisi string tutup yang diteruskan saat peristiwa close() dialog dipanggil. Dalam peristiwa dialogClosed, sangat penting untuk mengetahui apakah dialog ditutup, dibatalkan, atau dikonfirmasi. Jika dikonfirmasi, skrip akan mengambil nilai formulir dan mereset formulir. Reset ini berguna agar saat dialog ditampilkan kembali, dialog tersebut kosong dan siap menerima pengiriman baru.

Kesimpulan

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

Mari kita diversifikasi pendekatan kami dan mempelajari semua cara untuk membangun di web.

Buat demo, tweet link-nya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.

Remix komunitas

Resource