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.
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:
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>
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.
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
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;
}
}
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:
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;
}
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:
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;
}
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);
}
}
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;
}
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);
}
}
Menata gaya dialog <footer>
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);
}
}
Menata gaya menu footer dialog
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;
}
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:
- Pengurangan gerakan adalah transisi default, yaitu opasitas sederhana yang memudar dan muncul.
- Jika gerakannya sudah baik, animasi geser dan skala akan ditambahkan.
- 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
- @GrimLink dengan dialog 3-in-1.
- @mikemai2awesome dengan remix
bagus yang tidak mengubah
properti
display
. - @geoffrich_ dengan Svelte dan polesan Svelte FLIP yang bagus.
Resource
- Kode sumber di GitHub
- Avatar Doodle