Ringkasan dasar tentang cara membangun modal mini dan mega yang adaptif warna, responsif, dan mudah diakses dengan elemen <dialog>
.
Dalam postingan ini, saya ingin berbagi pendapat tentang cara mem-build modal mini dan mega yang adaptif warna,
responsif, dan mudah diakses dengan elemen <dialog>
.
Coba demo dan lihat sumbernya.
Jika Anda lebih suka video, berikut versi YouTube postingan ini:
Ringkasan
Elemen
<dialog>
sangat cocok untuk tindakan atau informasi kontekstual dalam halaman. Pertimbangkan kapan
pengalaman pengguna dapat memperoleh manfaat dari tindakan halaman yang sama, bukan tindakan multi-halaman,
mungkin karena formulirnya kecil atau satu-satunya tindakan yang diperlukan dari
pengguna adalah konfirmasi atau batalkan.
Elemen <dialog>
baru-baru ini menjadi stabil di seluruh browser:
Saya menemukan bahwa elemen 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 ini akan otomatis disembunyikan dan memiliki gaya bawaan untuk menempatkan konten Anda.
<dialog>
…
</dialog>
Kami dapat meningkatkan garis dasar ini.
Secara tradisional, elemen dialog banyak berbagi dengan modal, dan sering kali namanya
dapat dipertukarkan. Di sini saya membebaskan penggunaan elemen dialog untuk
popup dialog kecil (mini), serta dialog halaman penuh (mega). Saya menamainya
mega dan mini, dengan kedua dialog sedikit diadaptasi untuk kasus penggunaan yang berbeda.
Saya telah 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 digabungkan.
Merupakan ide baik untuk memiliki 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>
.
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 saya merasa praktik terbaiknya adalah meletakkannya di
tombol batal, bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan secara sengaja dan tidak disengaja.
Dialog mini
Dialog mininya 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 tampilan penuh yang dapat mengumpulkan data dan interaksi pengguna. Hal-hal dasar ini dapat menciptakan interaksi yang sangat menarik dan efektif di situs atau aplikasi Anda.
Aksesibilitas
Elemen dialog memiliki aksesibilitas bawaan yang sangat baik. Alih-alih menambahkan fitur-fitur ini seperti yang biasa saya lakukan, banyak fitur yang sudah ada di sana.
Memulihkan fokus
Seperti yang kami lakukan secara manual dalam Membuat komponen sidenav, penting bahwa membuka dan menutup sesuatu dengan benar akan berfokus pada tombol buka dan tutup yang relevan. Saat panel samping tersebut terbuka, fokus terdapat pada tombol tutup. Saat tombol tutup ditekan, fokus dipulihkan ke tombol yang membukanya.
Elemen dialog ini merupakan 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 pada dokumen. Sebelum inert
, JavaScript digunakan untuk mengamati fokus saat meninggalkan elemen, pada saat itu JavaScript mencegat dan menempatkannya kembali.
Setelah inert
, setiap bagian dokumen dapat "dibekukan" sedemikian rupa sehingga
tidak lagi menjadi target fokus atau interaktif dengan mouse. Alih-alih membatasi fokus, fokus akan diarahkan ke satu-satunya bagian interaktif dalam dokumen.
Membuka dan memfokuskan otomatis elemen
Secara default, elemen dialog akan menetapkan fokus ke elemen pertama yang dapat difokuskan
dalam markup dialog. Jika ini bukan elemen terbaik yang menjadi default bagi pengguna,
gunakan atribut autofocus
. Seperti yang dijelaskan sebelumnya, praktik terbaik adalah
menempatkan ini pada tombol batal dan bukan tombol konfirmasi. Hal ini memastikan bahwa konfirmasi dilakukan secara sengaja dan tidak disengaja.
Menutup dengan tombol escape
Sangat penting untuk mempermudah penutupan elemen yang berpotensi mengganggu ini. Untungnya, elemen dialog akan menangani kunci escape untuk Anda, sehingga membebaskan Anda dari beban orkestrasi.
Gaya
Ada jalur mudah untuk menata gaya elemen dialog dan jalur keras. Jalur
mudah dicapai dengan tidak mengubah properti tampilan dialog dan menangani
batasannya. Saya bekerja di jalur sulit untuk menyediakan animasi kustom untuk
membuka dan menutup dialog, mengambil alih properti display
dan banyak lagi.
Menata Gaya dengan Props Terbuka
Untuk mempercepat warna adaptif dan konsistensi desain secara keseluruhan, saya tanpa malu-malu membawa library variabel CSS Open Props. Selain variabel yang disediakan gratis, saya juga mengimpor file normalisasi 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 display
Perilaku tampilkan dan sembunyikan default dari elemen dialog akan mengalihkan properti
tampilan dari block
menjadi none
. Sayangnya, ini berarti animasi tidak dapat
dianimasikan masuk dan keluar, hanya di dalam. Saya ingin menganimasikan masuk dan keluar, dan langkah pertamanya adalah
menetapkan properti
display saya sendiri:
dialog {
display: grid;
}
Dengan mengubah, dan juga memiliki, nilai properti tampilan, seperti 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, sehingga memastikan
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 ke preferensi sistem terang dan gelap, saya ingin menyesuaikan
elemen dialog lebih dari itu. Open Props menyediakan beberapa warna
platform yang beradaptasi secara otomatis dengan
preferensi sistem terang dan gelap, mirip dengan menggunakan color-scheme
. Metode ini
sangat bagus untuk membuat lapisan dalam desain dan saya suka menggunakan warna untuk
mendukung tampilan permukaan lapisan ini secara visual. Warna latar belakang 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. Elemen ini sangat diperlukan untuk elemen dialog, tetapi sangat penting untuk membuat desain dialog yang menarik dan dirancang dengan baik.
Ukuran dialog responsif
Dialog ini secara default mendelegasikan ukurannya ke kontennya, yang umumnya
besar. Sasaran saya di sini adalah membatasi
max-inline-size
pada ukuran yang dapat dibaca (--size-content-3
= 60ch
) atau 90% dari lebar area tampilan. Hal ini
memastikan dialog tidak akan melebar di perangkat seluler, dan tidak akan terlalu
lebar pada 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 dialog yang dapat di-scroll, jika berupa elemen dialog
yang 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 ingin saya lakukan adalah mempertahankan dialog dalam alur relatif,
untuk pengguna internasional, jadi saya menggunakan unit dvb
yang logis, lebih baru, dan hanya
didukung sebagian di deklarasi kedua saat menjadi lebih stabil.
Pemosisian dialog mega
Untuk membantu Anda memosisikan elemen dialog, ada baiknya menguraikan dua bagiannya: tampilan latar layar penuh dan penampung dialog. Tampilan latar harus mencakup semuanya, memberikan efek bayangan untuk membantu mendukung bahwa dialog ini berada di depan dan konten di belakang tidak dapat diakses. Penampung dialog dapat dipusatkan pada tampilan latar ini secara bebas dan mengambil bentuk apa pun yang diperlukan kontennya.
Gaya berikut memperbaiki elemen dialog ke jendela, merentangkannya 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 modal besar halaman penuh ini dengan cara yang sedikit berbeda. Saya
menyetel margin bawah ke 0
, yang membawa konten dialog ke bagian bawah
area pandang. Dengan beberapa penyesuaian gaya, saya bisa 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;
}
}
Pemosisian dialog mini
Saat menggunakan area pandang yang lebih besar seperti pada komputer desktop, saya memilih untuk menempatkan dialog mini di atas elemen yang memanggilnya. Untuk melakukannya, saya memerlukan JavaScript. Anda dapat menemukan teknik yang saya gunakan di sini, tetapi saya merasa itu di luar cakupan artikel ini. Tanpa JavaScript, dialog mini akan muncul di tengah layar, seperti dialog mega.
Buat konten yang menarik
Terakhir, tambahkan beberapa gaya ke dialog sehingga terlihat seperti permukaan halus yang berada jauh di atas halaman. Kelembutan dicapai dengan membulatkan sudut dialog. Kedalamannya dapat dicapai dengan salah satu properti bayangan yang dibuat dengan cermat oleh Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Menyesuaikan elemen pseudo tampilan latar
Saya memilih untuk bekerja sangat ringan dengan tampilan latar, 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 bahwa browser
akan memungkinkan transisi elemen tampilan latar di masa mendatang:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Menata gaya tambahan
Saya menyebut bagian ini "tambahan" karena lebih berkaitan dengan demo elemen dialog 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 menurut
spesifikasi,
ini tidak berpengaruh pada dialog karena bukan port scroll, artinya, ini bukan
scroller sehingga tidak ada yang perlu dicegah. Saya dapat menggunakan JavaScript untuk memantau peristiwa baru dari panduan ini, seperti "closed" dan "opened", serta mengaktifkan overflow: hidden
pada dokumen, atau saya dapat menunggu :has()
stabil di semua browser:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Sekarang ketika 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,
{i>footer<i} dan artikel. Dengan tata letak ini, saya ingin menjabarkan 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 pasti dan ukuran baris pasti ini memungkinkan
elemen artikel dibatasi dan dapat di-scroll saat melebihi:
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. Tombol ini juga diberi warna permukaan agar tampak berada di belakang konten artikel dialog. Persyaratan ini menyebabkan container flexbox, item yang disejajarkan secara vertikal dan berjarak ke tepinya, serta beberapa padding dan celah untuk memberi 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 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: ini adalah ruang yang dimaksudkan untuk di-scroll jika muncul dialog yang tinggi atau panjang.
Untuk melakukannya, elemen formulir induk telah menetapkan beberapa batas maksimum untuk
dirinya sendiri yang memberikan batasan untuk dicapai elemen artikel ini jika
terlalu tinggi. Tetapkan overflow-y: auto
sehingga scroll bar hanya ditampilkan saat diperlukan,
berisi scroll 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 {i>footer<i} adalah berisi menu tombol tindakan. Flexbox digunakan untuk meratakan konten ke akhir sumbu sejajar footer, lalu beberapa spasi untuk memberikan 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. Fungsi ini menggunakan tata letak
flexbox penggabungan dengan gap
untuk memberikan ruang di antara tombol-tombol. Elemen menu
memiliki padding seperti <ul>
. Saya juga menghapus gaya itu
karena saya tidak membutuhkannya.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
Animasi
Elemen dialog sering dianimasikan karena masuk dan keluar dari jendela. Memberikan beberapa gerakan yang mendukung pada dialog untuk masuk dan keluar ini akan membantu pengguna mengorientasikan diri mereka dalam alur.
Biasanya elemen dialog hanya bisa dianimasikan masuk, bukan keluar. Hal ini karena browser mengalihkan properti display
pada elemen. Sebelumnya, panduan
menyetelnya untuk ditampilkan ke petak, dan tidak pernah menetapkannya ke tidak ada. Hal ini membuka kemampuan untuk
menganimasikan masuk dan keluar.
Open Props dilengkapi dengan banyak animasi keyframe untuk digunakan, yang membuat orkestrasi mudah dan dapat dibaca. Berikut adalah tujuan animasi dan pendekatan berlapis yang saya ambil:
- Gerakan yang diperkecil adalah transisi default, yaitu opasitas sederhana yang memudar dan memudar.
- Jika tidak ada masalah dengan gerakan, animasi geser dan skala akan ditambahkan.
- Tata letak seluler responsif untuk dialog mega disesuaikan agar dapat bergeser keluar.
Transisi default yang aman dan bermakna
Meskipun Open Props dilengkapi dengan keyframe untuk memudar dan memudar, saya lebih menyukai
pendekatan transisi berlapis ini sebagai default dengan animasi keyframe sebagai
potensi upgrade. Sebelumnya, kita sudah menata gaya visibilitas dialog dengan
opacity, mengatur 1
atau 0
, bergantung pada atribut [open]
. Untuk
transisi antara 0% dan 100%, beri tahu browser durasi dan jenis
easing yang Anda inginkan:
dialog {
transition: opacity .5s var(--ease-3);
}
Menambahkan gerakan ke transisi
Jika pengguna setuju dengan gerakan, dialog mega dan mini akan bergeser
ke atas sebagai pintu masuk, dan diskalakan sebagai 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 seluler
Sebelumnya di bagian gaya visual, gaya dialog mega disesuaikan untuk perangkat seluler agar lebih mirip dengan lembar tindakan, seolah-olah selembar kertas kecil telah meluncur ke atas dari bagian bawah layar dan masih menempel di bagian bawah. Animasi keluar scale out kurang cocok dengan desain baru ini, dan kita dapat menyesuaikannya dengan beberapa kueri media dan beberapa Open Props:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
Ada beberapa hal yang perlu ditambahkan dengan JavaScript:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
Penambahan ini berasal dari keinginan untuk menutup cahaya (mengklik backdrop dialog), animasi, dan beberapa peristiwa tambahan untuk pengaturan waktu yang lebih baik dalam mendapatkan data formulir.
Menambahkan tutup ringan
Tugas ini mudah dan merupakan tambahan yang bagus untuk elemen dialog yang tidak
dianimasikan. Interaksi dicapai dengan melihat klik pada elemen
dialog dan memanfaatkan gelembung
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 disediakan.
String ini dapat diambil oleh JavaScript lain untuk mendapatkan insight tentang bagaimana
dialog ditutup. Anda akan menemukan bahwa saya juga telah menyediakan {i>close string<i} setiap kali memanggil
fungsi dari berbagai tombol, untuk memberikan konteks ke aplikasi saya tentang
interaksi pengguna.
Menambahkan peristiwa penutupan dan acara tertutup
Elemen dialog disertai dengan peristiwa tutup: elemen ini langsung muncul saat
fungsi close()
dialog dipanggil. Karena kita menganimasikan elemen ini, sebaiknya
ada peristiwa sebelum dan setelah animasi, agar perubahan 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,
proses peristiwa tutup bawaan pada dialog. Dari sini, setel dialog ke
inert
dan kirim peristiwa closing
. Tugas berikutnya adalah menunggu
animasi dan transisi selesai berjalan pada 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 Membuat komponen
toast, menampilkan promise berdasarkan penyelesaian animasi dan promise transisi. Itulah sebabnya dialogClose
adalah fungsi asinkron; promise tersebut kemudian dapat await
ditampilkan dan bergerak maju dengan percaya diri ke peristiwa yang ditutup.
Menambahkan peristiwa terbuka dan terbuka
Peristiwa ini tidak mudah ditambahkan karena elemen dialog bawaan tidak menyediakan peristiwa terbuka seperti halnya dengan close. Saya menggunakan MutationObserver untuk memberikan insight tentang perubahan atribut dialog. Dalam observer ini, saya akan memantau perubahan pada atribut terbuka dan mengelola peristiwa kustom sebagaimana mestinya.
Sama halnya dengan cara kita memulai peristiwa penutup dan peristiwa tertutup, buat dua peristiwa baru yang disebut opening
dan opened
. Jika sebelumnya kita memproses peristiwa tutup dialog, kali ini gunakan observer mutasi yang dibuat untuk mengamati 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 observer mutasi akan dipanggil saat atribut dialog
diubah, yang menyediakan daftar perubahan sebagai array. Lakukan iterasi pada
perubahan atribut, dengan mencari attributeName
yang akan terbuka. Selanjutnya, periksa
apakah elemen tersebut memiliki atribut atau tidak: ini memberi tahu apakah dialog
telah 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 peristiwa tertutup, segera kirim peristiwa pembuka, tunggu animasinya 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. Hal ini berguna untuk membersihkan peristiwa atau data saat dialog dihapus.
Anda dapat melakukannya dengan observer mutasi lain. Kali ini, kita akan mengamati turunan elemen isi dan memperhatikan elemen dialog yang dihapus, bukan mengamati atribut pada 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 mutasi observer dipanggil setiap kali turunan ditambahkan atau dihapus dari isi dokumen. Mutasi spesifik yang dipantau adalah untuk
removedNodes
yang memiliki
nodeName
dialog. Jika dialog dihapus, peristiwa klik dan peristiwa tutup akan 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 pemuatan halaman, atribut pemuatan telah ditambahkan ke dialog. Skrip berikut menunggu animasi dialog selesai berjalan, lalu menghapus atribut. Sekarang dialog dapat dianimasikan masuk dan keluar, dan kita telah secara efektif menyembunyikan animasi yang mengganggu.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Pelajari lebih lanjut masalah mencegah animasi keyframe saat pemuatan halaman di sini.
Semuanya
Berikut adalah dialog.js
secara keseluruhan, setelah kami menjelaskan setiap bagian
satu per satu:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Menggunakan modul dialog.js
Fungsi yang diekspor dari modul ini 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 tersebut 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 dan data formulir untuk menambahkan elemen avatar baru ke daftar. Pengaturan waktunya tepat karena dialog telah menyelesaikan animasi keluarnya, lalu beberapa skrip dianimasikan di avatar baru. Berkat peristiwa baru ini, orkestrasi pengalaman pengguna dapat lebih lancar.
Perhatikan dialog.returnValue
: file ini berisi string penutup yang diteruskan saat
peristiwa close()
dialog dipanggil. 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 ditampilkan lagi, dialog tersebut kosong dan siap untuk pengiriman baru.
Kesimpulan
Setelah Anda tahu cara saya melakukannya, bagaimana Anda‽ 🙂
Mari lakukan diversifikasi pendekatan dan pelajari semua cara untuk membangun di web.
Buat demo, link tweet me, 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 polesan Svelte FLIP yang bagus.
Referensi
- Kode sumber di GitHub
- Avatar Doodle