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 demonya dan lihat
sumbernya.
Jika Anda lebih suka menonton video, berikut versi YouTube dari postingan ini:
Ringkasan
Elemen
<dialog>
sangat cocok untuk informasi atau tindakan kontekstual dalam halaman. Pertimbangkan kapan pengalaman pengguna dapat memperoleh manfaat dari tindakan di halaman yang sama, bukan tindakan multi-halaman: mungkin karena formulirnya kecil atau satu-satunya tindakan yang diperlukan dari pengguna adalah mengonfirmasi atau membatalkan.
Elemen <dialog>
baru-baru ini menjadi stabil di berbagai browser:
Saya menemukan 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, dan jenis huruf mini dan mega.
Markup
Dasar-dasar elemen <dialog>
cukup sederhana. Elemen akan
disembunyikan secara otomatis dan memiliki gaya bawaan untuk menempatkan konten Anda.
<dialog>
…
</dialog>
Kita dapat meningkatkan kualitas dasar ini.
Secara tradisional, elemen dialog memiliki banyak kesamaan dengan modal, dan sering kali nama keduanya dapat dipertukarkan. Di sini, saya menggunakan elemen dialog untuk
pop-up dialog kecil (mini), serta dialog halaman penuh (mega). Saya menamainya mega dan mini, dengan kedua dialog sedikit disesuaikan untuk kasus penggunaan yang berbeda.
Saya menambahkan atribut modal-mode
agar Anda dapat menentukan jenisnya:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Tidak selalu, tetapi elemen dialog umumnya akan digunakan untuk mengumpulkan beberapa
informasi interaksi. Formulir di dalam elemen dialog dibuat agar
bersama-sama.
Sebaiknya buat elemen formulir yang membungkus konten dialog Anda agar
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 besar
Dialog mega memiliki tiga elemen di dalam formulir:
<header>
,
<article>
,
dan
<footer>
.
Elemen ini berfungsi sebagai penampung semantik, serta target gaya untuk
presentasi dialog. Header memberi judul modal dan menawarkan tombol tutup. Artikel ini ditujukan untuk input dan informasi formulir. Footer berisi
<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 menurut saya praktik terbaiknya adalah menempatkan ini di
tombol batalkan, bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan dengan sengaja dan bukan secara tidak sengaja.
Dialog mini
Dialog mini sangat mirip dengan dialog mega, hanya saja tidak memiliki elemen
<header>
. Hal ini memungkinkan ikon menjadi lebih kecil dan lebih sejajar.
<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 viewport 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 sudah tersedia.
Memulihkan fokus
Seperti yang kita lakukan secara manual di Membangun komponen sidenav, penting agar membuka dan menutup sesuatu dengan benar memfokuskan pada tombol buka dan tutup yang relevan. Saat sidenav terbuka, fokus akan berada di 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.
Memerangkap fokus
Elemen dialog mengelola
inert
untuk Anda di dokumen. Sebelum inert
, JavaScript digunakan untuk memantau fokus
yang keluar dari elemen, yang kemudian mencegat dan menempatkannya kembali.
Setelah inert
, bagian dokumen apa pun dapat "dibekukan" sehingga tidak lagi menjadi target fokus atau interaktif dengan mouse. Daripada menjebak fokus, fokus 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 yang akan digunakan pengguna secara default,
gunakan atribut autofocus
. Seperti yang dijelaskan sebelumnya, menurut saya praktik terbaiknya adalah
menempatkan ini di tombol batal, bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan dengan sengaja dan bukan secara tidak sengaja.
Menutup dengan tombol escape
Penting untuk mempermudah penutupan elemen yang berpotensi mengganggu ini. Untungnya, elemen dialog akan menangani tombol escape untuk Anda, sehingga Anda tidak perlu repot-repot melakukan orkestrasi.
Gaya
Ada jalur mudah untuk menata gaya elemen dialog dan jalur sulit. Jalur
mudah dicapai dengan tidak mengubah properti tampilan dialog dan bekerja
dengan batasannya. Saya memilih jalur sulit untuk menyediakan animasi kustom saat
membuka dan menutup dialog, mengambil alih properti display
, dan lainnya.
Menata gaya dengan Open Props
Untuk mempercepat warna adaptif dan konsistensi desain secara keseluruhan, saya dengan berani menggunakan library variabel CSS Open Props. Selain variabel gratis yang disediakan, saya juga mengimpor file normalize dan beberapa tombol, yang keduanya disediakan 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 tampil dan sembunyi default elemen dialog mengalihkan properti
tampilan dari block
ke none
. Sayangnya, ini berarti elemen tidak dapat dianimasikan
saat masuk dan keluar, hanya saat masuk. Saya ingin menganimasikan masuk dan keluar, dan langkah pertama adalah
menetapkan properti
display saya sendiri:
dialog {
display: grid;
}
Dengan mengubah, dan oleh karena itu 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 adalah tertutup. 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, sehingga
pengguna keyboard dan pembaca layar juga tidak dapat menjangkau dialog tersembunyi.
Memberi dialog tema warna adaptif
Meskipun color-scheme
mengikutsertakan dokumen Anda dalam tema warna adaptif yang disediakan browser untuk preferensi sistem terang dan gelap, saya ingin menyesuaikan elemen dialog lebih dari itu. Open Props menyediakan beberapa warna
permukaan yang beradaptasi secara otomatis dengan
preferensi sistem terang dan gelap, mirip dengan penggunaan color-scheme
. Warna ini sangat cocok 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);
}
}
Warna yang lebih 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 didesain dengan baik.
Penentuan ukuran dialog responsif
Secara default, dialog mendelegasikan ukurannya ke kontennya, yang umumnya
bagus. Tujuan saya di sini adalah membatasi
max-inline-size
ke ukuran yang dapat dibaca (--size-content-3
= 60ch
) atau 90% dari lebar area pandang. Hal ini
memastikan dialog tidak ditampilkan di layar penuh pada perangkat seluler, dan tidak terlalu
lebar di layar desktop sehingga sulit dibaca. Kemudian, saya menambahkan
max-block-size
agar dialog tidak melebihi tinggi halaman. Artinya, kita juga perlu menentukan lokasi area scroll dialog, 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 bagaimana saya memiliki max-block-size
dua kali? Yang pertama menggunakan 80vh
, unit
area tampilan fisik. Yang saya inginkan adalah menjaga dialog dalam alur relatif,
untuk pengguna internasional, jadi saya menggunakan unit dvb
yang logis, lebih baru, dan hanya didukung sebagian
dalam deklarasi kedua saat unit tersebut menjadi lebih stabil.
Pemosisian dialog mega
Untuk membantu memosisikan elemen dialog, ada baiknya memecah dua bagiannya: latar belakang layar penuh dan penampung dialog. Latar belakang harus menutupi semuanya, memberikan efek bayangan untuk membantu mendukung bahwa dialog ini berada di depan dan konten di belakangnya 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, meregangkannya 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 tampilan kecil, saya menata modal mega halaman penuh ini sedikit berbeda. Saya
menetapkan margin bawah ke 0
, yang membawa konten dialog ke bagian bawah
area pandang. Dengan beberapa penyesuaian gaya, saya dapat mengubah dialog menjadi
actionsheet, yang 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;
}
}
Penempatan dialog mini
Saat menggunakan area tampilan 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 saya merasa hal ini di luar cakupan artikel ini. Tanpa JavaScript, dialog mini akan muncul di tengah layar, seperti dialog mega.
Buat desain tampil menarik
Terakhir, tambahkan sentuhan unik pada dialog agar terlihat seperti permukaan lembut yang berada jauh di atas halaman. Kelembutan dicapai dengan membulatkan sudut dialog. Kedalaman dicapai dengan salah satu shadow props Open Props yang dibuat dengan cermat:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Menyesuaikan elemen semu latar belakang
Saya memilih untuk bekerja dengan latar belakang secara ringan, hanya menambahkan efek blur dengan
backdrop-filter
ke dialog mega:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
Saya juga memilih untuk menerapkan transisi pada backdrop-filter
, dengan harapan browser akan mengizinkan transisi elemen latar belakang di masa mendatang:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Ekstra gaya
Saya menyebut bagian ini "tambahan" karena lebih berkaitan dengan demo elemen dialog saya daripada elemen dialog secara umum.
Penampungan scroll
Saat dialog ditampilkan, pengguna masih dapat men-scroll halaman di belakangnya, yang tidak saya inginkan:
Biasanya,
overscroll-behavior
adalah solusi yang biasa saya gunakan, tetapi menurut
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 memantau
peristiwa baru dari panduan ini, seperti "tertutup" dan "terbuka", serta mengalihkan
overflow: hidden
pada dokumen, atau saya dapat menunggu hingga :has()
stabil di
semua browser:
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 bermaksud mengartikulasikan turunan artikel sebagai area yang dapat di-scroll. Saya melakukannya dengan
grid-template-rows
.
Elemen artikel diberi 1fr
dan formulir itu sendiri memiliki tinggi maksimum yang sama dengan elemen dialog. Menetapkan tinggi tetap dan ukuran baris tetap inilah yang
memungkinkan elemen artikel dibatasi dan di-scroll saat meluap:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Menata gaya dialog <header>
Peran elemen ini adalah untuk memberikan judul pada konten dialog dan menawarkan tombol tutup yang mudah ditemukan. Elemen ini juga diberi warna permukaan agar tampak berada di belakang konten artikel dialog. Persyaratan ini menghasilkan penampung flexbox, item yang disusun secara vertikal yang diberi jarak ke tepinya, dan beberapa padding dan jarak untuk memberi ruang bagi 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);
}
}
Menata gaya tombol tutup header
Karena demo menggunakan tombol Open Props, tombol tutup disesuaikan menjadi tombol berpusat pada ikon bulat seperti berikut:
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;
}
Menata gaya dialog <article>
Elemen artikel memiliki peran khusus dalam dialog ini: elemen ini adalah ruang yang dimaksudkan untuk di-scroll jika dialognya tinggi atau panjang.
Untuk melakukannya, elemen formulir induk telah menetapkan beberapa nilai maksimum untuk
dirinya sendiri yang memberikan batasan bagi elemen artikel ini untuk dicapai jika
tingginya terlalu besar. Tetapkan overflow-y: auto
sehingga scrollbar hanya ditampilkan jika diperlukan, sertakan scrolling di dalamnya dengan overscroll-behavior: contain
, dan sisanya akan menjadi 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);
}
}
Menata gaya dialog <footer>
Peran footer adalah untuk memuat menu tombol tindakan. Flexbox digunakan untuk menyelaraskan konten ke akhir sumbu inline footer, lalu beberapa jarak untuk memberi ruang pada tombol.
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);
}
}
Menata gaya menu footer dialog
Elemen menu
digunakan untuk memuat tombol tindakan untuk dialog. Tata letak flexbox yang dilipat
dengan gap
digunakan 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;
}
Animasi
Elemen dialog sering kali dianimasikan karena masuk dan keluar dari jendela. Memberikan beberapa gerakan pendukung untuk masuk dan keluar dialog ini membantu pengguna mengorientasikan diri dalam alur.
Biasanya, elemen dialog hanya dapat dianimasikan saat masuk, bukan saat keluar. Hal ini karena
browser mengalihkan properti display
pada elemen. Sebelumnya, panduan
menetapkan tampilan ke petak, dan tidak pernah menetapkannya ke tidak ada. Hal ini memungkinkan Anda
membuat animasi masuk dan keluar.
Open Props dilengkapi dengan banyak animasi keyframe untuk digunakan, yang membuat orkestrasi menjadi mudah dan jelas. Berikut adalah tujuan animasi dan pendekatan berlapis yang saya lakukan:
- Gerakan yang dikurangi adalah transisi default, yaitu memudar masuk dan keluar dengan opasitas sederhana.
- Jika gerakan tidak masalah, animasi geser dan skala akan ditambahkan.
- Tata letak seluler responsif untuk dialog besar disesuaikan agar dapat meluncur keluar.
Transisi default yang aman dan bermakna
Meskipun Open Props dilengkapi dengan keyframe untuk memudar dan keluar, saya lebih memilih pendekatan transisi berlapis ini sebagai default dengan animasi keyframe sebagai potensi upgrade. Sebelumnya, kita telah menata gaya visibilitas dialog dengan
opasitas, mengatur 1
atau 0
bergantung pada atribut [open]
. Untuk
melakukan transisi antara 0% dan 100%, beri tahu browser durasi dan jenis
percepatan yang Anda inginkan:
dialog {
transition: opacity .5s var(--ease-3);
}
Menambahkan gerakan ke transisi
Jika pengguna tidak keberatan dengan gerakan, dialog mega dan mini harus meluncur
ke atas saat muncul, dan mengecil saat keluar. Anda dapat melakukannya dengan kueri media
prefers-reduced-motion
dan beberapa Open Props:
@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, gaya dialog mega diadaptasi untuk perangkat seluler agar lebih seperti sheet tindakan, seolah-olah selembar kecil kertas telah meluncur ke atas dari bagian bawah layar dan masih terpasang di bagian bawah. Animasi keluar skala tidak sesuai 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 dapat 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 pengaturan waktu yang lebih baik dalam mendapatkan data formulir.
Menambahkan penutupan ringan
Tugas ini mudah dan merupakan tambahan yang bagus untuk elemen dialog yang tidak
beranimasi. Interaksi dicapai dengan memantau klik pada elemen dialog dan memanfaatkan bubbling peristiwa untuk menilai apa yang diklik, dan hanya akan close()
jika elemen tersebut 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 penutupan dialog. Anda akan melihat bahwa saya juga memberikan string penutup setiap kali saya memanggil
fungsi dari berbagai tombol, untuk memberikan konteks ke aplikasi saya tentang
interaksi pengguna.
Menambahkan acara penutup dan tertutup
Elemen dialog dilengkapi dengan peristiwa penutupan: peristiwa ini segera dipancarkan saat fungsi close()
dialog dipanggil. Karena kita menganimasikan elemen ini, sebaiknya ada peristiwa sebelum dan setelah animasi, untuk perubahan guna mengambil data atau mereset formulir dialog. Saya menggunakannya di sini untuk mengelola penambahan atribut
inert
pada dialog yang tertutup, dan dalam demo, saya menggunakannya untuk mengubah
daftar avatar jika pengguna telah mengirimkan gambar baru.
Untuk melakukannya, buat dua acara baru bernama closing
dan closed
. Kemudian
dengarkan peristiwa penutupan bawaan pada dialog. Dari sini, tetapkan dialog ke
inert
dan kirimkan peristiwa closing
. Tugas berikutnya adalah menunggu animasi dan transisi selesai berjalan pada dialog, lalu mengirimkan 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 Membangun komponen toast, menampilkan promise berdasarkan penyelesaian promise animasi dan transisi. Itulah sebabnya dialogClose
adalah fungsi
asinkron;
fungsi ini kemudian dapat
await
Promise yang ditampilkan dan melanjutkan dengan yakin ke peristiwa tertutup.
Menambahkan acara pembukaan dan acara yang dibuka
Peristiwa ini tidak mudah ditambahkan karena elemen dialog bawaan tidak menyediakan peristiwa terbuka seperti yang dilakukan dengan peristiwa tertutup. Saya menggunakan MutationObserver untuk memberikan insight tentang perubahan atribut dialog. Dalam pengamat ini, saya akan memantau perubahan pada atribut terbuka dan mengelola peristiwa kustom dengan tepat.
Mirip dengan cara kita memulai penutupan dan menutup acara, buat dua acara baru yang disebut opening
dan opened
. Jika sebelumnya kita memantau peristiwa penutupan dialog, kali ini kita menggunakan pengamat mutasi yang dibuat untuk memantau 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 memberi tahu apakah dialog telah terbuka atau tidak. Jika sudah 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, kirimkan peristiwa pembukaan segera, tunggu hingga animasi selesai, lalu kirimkan peristiwa dibuka.
Menambahkan acara yang dihapus
Dalam aplikasi web satu halaman, dialog sering ditambahkan dan dihapus berdasarkan rute atau kebutuhan dan status aplikasi lainnya. Hal ini dapat berguna untuk 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 elemen turunan dari elemen body dan mengamati penghapusan elemen dialog.
…
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 diamati adalah untuk
removedNodes
yang memiliki
nodeName
dari
dialog. Jika dialog dihapus, peristiwa klik dan tutup akan dihapus untuk
mengosongkan memori, dan peristiwa dihapus kustom akan dikirim.
Menghapus atribut pemuatan
Untuk mencegah animasi dialog memutar animasi keluar saat ditambahkan ke halaman atau saat halaman dimuat, atribut pemuatan telah ditambahkan ke dialog. Skrip berikut menunggu hingga animasi dialog selesai berjalan, lalu menghapus atribut. Sekarang dialog dapat dianimasikan masuk dan keluar, dan kita telah 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.
Bersama-sama
Berikut adalah dialog.js
secara keseluruhan, setelah kita menjelaskan setiap bagiannya 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 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 untuk 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, mengatur pengalaman pengguna dapat menjadi lebih lancar.
Pemberitahuan dialog.returnValue
: ini berisi string tutup yang diteruskan saat peristiwa
dialog close()
dipanggil. Hal ini penting dalam peristiwa dialogClosed
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 lagi, dialog tersebut kosong dan siap untuk pengiriman baru.
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
- @GrimLink dengan dialog 3-in-1.
- @mikemai2awesome dengan remix
yang bagus yang tidak mengubah
properti
display
. - @geoffrich_ dengan Svelte dan Svelte FLIP yang bagus.
Resource
- Kode sumber di GitHub
- Avatar Doodle