Prompt dan Konteks yang Berlaku
Bangun modal dialog yang dapat digunakan kembali untuk sesi wawancara coding UI frontend. Pemanggil (caller) menyediakan judul, deskripsi singkat opsional, konten body, status terbuka (open state), serta callback konfirmasi dan penutupan. Dialog harus mendukung pengguna mouse, sentuhan (touch), keyboard, dan teknologi alat bantu (assistive technology). Pemusatan visual (centering) dan latar belakang redup (dimmed backdrop) diperlukan, tetapi masalah utamanya adalah perilaku: konten di luar dialog aktif tidak dapat diakses, dialog memiliki nama yang bermakna, fokus masuk dan tetap berada di dalamnya, setiap pengguna memiliki cara eksplisit untuk menutupnya, dan fokus kembali ke tempat yang logis setelahnya.
Asumsikan browser modern (evergreen) saat ini mendukung API dialog native. Jelaskan juga bagaimana kontrak ini akan diimplementasikan ketika pewawancara secara eksplisit melarang elemen native, webview yang disematkan tidak memiliki dukungan, atau pustaka komponen yang ada sudah memiliki primitif kustom yang teruji. Unit yang dapat digunakan kembali ini tidak boleh menetapkan target initial-focus secara hard-code karena formulir, dialog informasi yang panjang, dan konfirmasi penghapusan yang tidak dapat dibatalkan membutuhkan pilihan yang berbeda.
Implementasi ini tidak mencakup pengambilan data spesifik aplikasi, desain animasi, atau manajer modal yang lengkap. Hal-hal tersebut menjadi pertanyaan lanjutan. Kriteria penerimaan dapat diamati secara langsung: aktivasi keyboard, penempatan fokus, pengurungan fokus (focus containment), penamaan aksesibel, perilaku penutupan, pemulihan fokus, layering, dan perilaku yang stabil saat rerender dan unmount.
Apa yang Dinilai oleh Pewawancara
Sinyal pertama adalah apakah kandidat mendefinisikan "modal" sebagai kontrak perilaku. Panel di tengah dengan nilai penumpukan (stacking value) tinggi hanyalah sebuah overlay. Modal sejati membuat sisa dokumen menjadi inert, mengurung urutan tab, mengekspos semantik dialog, dan mengelola seluruh siklus hidup fokus. Menambahkan atribut ARIA tanpa membuat konten di luar tidak tersedia akan menghasilkan pohon aksesibilitas (accessibility tree) yang menyesatkan.
Sinyal kedua adalah pertimbangan platform. Elemen dialog native yang dibuka dengan showModal() akan masuk ke browser top layer, membuat backdrop, membuat elemen lain dalam dokumen yang sama menjadi inert, dan menyediakan perilaku keyboard modal. Membukanya dengan show() atau dengan menyetel atribut open akan menjadikannya non-modal. Jawaban yang kuat menggunakan primitif native ketika kontrak browser produk mengizinkannya, sambil tetap mampu menyatakan setiap invarian yang harus direproduksi oleh implementasi kustom.
Sinyal ketiga adalah kebijakan fokus daripada sekadar aturan hafalan "fokuskan tombol pertama". Formulir pendek dapat memfokuskan field pertama yang tidak valid atau field utama. Dialog panjang yang menyerupai dokumen sering kali harus memfokuskan judul statis dengan target fokus terprogram agar struktur awal dan semantiknya tetap tersedia. Konfirmasi yang tidak dapat dibatalkan pada awalnya harus memfokuskan tindakan yang paling tidak destruktif. Saat ditutup, fokus biasanya kembali ke elemen pembuka; jika elemen tersebut telah dihapus, pemanggil harus memilih item kerja logis berikutnya.
Sinyal keempat adalah kebenaran siklus hidup framework. Status browser imperatif dan status UI deklaratif tidak boleh melenceng. Memanggil showModal() dua kali, menghapus dialog yang terbuka sebelum menyinkronkan status, menutupnya tanpa memperbarui parent, atau memulihkan fokus ke node yang usang menyebabkan bug nyata. Event listener memerlukan pembersihan (cleanup), label unik harus tetap stabil, dan server rendering tidak boleh memanggil API browser selama render.
Sinyal terakhir adalah kualitas verifikasi. Pemeriksaan aksesibilitas otomatis dapat menangkap nama yang hilang atau atribut yang tidak valid, tetapi tidak dapat membuktikan bahwa fokus mendarat pada elemen yang tepat untuk alur kerja ini, bahwa Tab dan Shift+Tab tetap terkurung, bahwa Escape mengikuti kebijakan produk, atau bahwa fokus kembali ke tempat yang logis setelah pembuka menghilang. Hal-hal tersebut memerlukan pemeriksaan manual dengan keyboard dan pembaca layar (screen reader).
Pertanyaan untuk Diklarifikasi Sebelum Menjawab
- Apakah ini benar-benar modal? Jika pengguna harus tetap berinteraksi dengan halaman, gunakan dialog non-modal, popover, atau panel inline. Jangan melabelinya sebagai modal hanya karena ia melayang di atas konten.
- Bolehkah saya menggunakan elemen dialog native? Jika ya, gunakan
showModal()dan pertahankan perilaku default-nya. Jika tidak, terapkan semantik, inertness di luar, pengurungan fokus, Escape, dan pemulihan fokus secara eksplisit. - Konten apa yang muncul di dalamnya? Formulir sederhana, dokumen terstruktur yang panjang, dan alert memerlukan deskripsi aksesibel dan pilihan initial-focus yang berbeda.
- Bisakah operasi ini dibatalkan? Untuk penghapusan atau pembayaran, fokuskan Cancel atau kontrol lain yang paling tidak destruktif. Untuk dialog kelanjutan rutin, tindakan berikutnya yang paling mungkin mungkin lebih tepat.
- Bagaimana cara menutupnya? Selalu sediakan kontrol tutup atau batal yang terlihat. Klarifikasi tombol Escape, klik backdrop, keberhasilan pengiriman (submit), dan apakah input yang belum disimpan memerlukan langkah konfirmasi.
- Apa yang harus menerima fokus setelah ditutup? Biasanya elemen pembuka. Jika pembuatan item menghapus atau mengganti node tersebut, identifikasi penerus logis yang stabil sebelum menulis komponen.
- Bisakah dialog bertumpuk? Lebih disukai satu modal aktif. Jika penumpukan diperlukan, hanya dialog paling atas yang menangani penutupan, dan menutupnya akan memulihkan fokus di dalam dialog di bawahnya.
- Browser dan teknologi alat bantu apa yang masuk dalam cakupan? Ini menentukan apakah elemen native sudah cukup, memerlukan compatibility layer, atau harus diganti dengan primitif teruji yang sudah mapan.
Kerangka Jawaban 30 Detik
"Saya akan mendefinisikan enam invarian: accessible name, latar belakang inert, initial focus yang disengaja, urutan tab yang terkurung, penutupan eksplisit dan Escape, serta pemulihan fokus yang logis. Pada browser saat ini, saya akan menggunakan elemen dialog native dengan showModal() untuk top layer, backdrop, inertness, dan perilaku fokus inti. Initial focus mengikuti tugas: heading untuk konten panjang, field yang relevan untuk formulir, dan Cancel untuk tindakan yang tidak dapat dibatalkan. Di React, saya akan menyinkronkan controlled state dengan efek open dan close yang dijaga (guarded), mendengarkan langsung event cancel, dan memverifikasi pembukaan keyboard, kedua arah tab, penutupan, penamaan screen reader, pemulihan fokus, rerender, dan dialog bertumpuk."
Pembahasan Mendalam Langkah demi Langkah
Mulailah dengan invarian yang independen dari React, CSS, atau elemen native:
OPEN
exactly one active modal owns interaction
outside content is visually obscured and behaviorally inert
dialog has an accessible name from a visible title
focus is inside the dialog on a purposefully selected target
WHILE OPEN
Tab and Shift+Tab cannot enter the background document
visible close or cancel control is reachable
only the top modal handles a close request
CLOSE
parent open state and browser dialog state agree
focus returns to the opener if it exists
otherwise focus moves to a predefined logical successorPilih primitif berikutnya. Dialog native adalah pilihan default untuk kontrak browser saat ini. Memanggil showModal() akan menempatkannya di top layer, memberinya backdrop, dan membuat konten lain dalam dokumennya menjadi inert. Ini menghindari penelusuran manual setiap elemen yang dapat difokuskan, penyetelan aria-hidden di seluruh root aplikasi, dan pertentangan stacking context. Memanggil show() atau menyetel open menghasilkan dialog non-modal, sehingga keduanya bukan jalan pintas yang dapat dipertukarkan.
Dialog kustom tetap valid ketika wawancara melarang elemen native atau produk sudah menggunakan primitif komponen yang matang. Dialog kustom harus me-render kontainer dengan semantik dialog dan accessible name, menetapkan semantik modal hanya ketika interaksi di luar benar-benar diblokir, membuat semua root latar belakang menjadi inert, mengurung fokus, menangani Escape, memulihkan fokus, dan me-render di atas stacking context aplikasi. Memindahkan panel ke document body menggunakan portal membantu masalah clipping dan stacking, tetapi portal saja tidak menyediakan perilaku aksesibilitas tersebut.
Gunakan tabel keputusan initial-focus:
- Untuk formulir pendek, fokuskan field pertama yang harus ditindaklanjuti pengguna, terutama field yang tidak valid setelah validasi.
- Untuk teks panjang, daftar, atau tabel, fokuskan judul atau elemen statis lainnya di awal dengan target fokus terprogram. Jangan meratakan struktur yang kaya menjadi satu deskripsi aksesibel yang panjang.
- Untuk pekerjaan yang tidak dapat dibatalkan, fokuskan Cancel atau tindakan yang paling tidak destruktif.
- Untuk konfirmasi sederhana atau kelanjutan, fokuskan tindakan yang paling mungkin terjadi jika hal itu tidak dapat memicu bahaya yang tidak disengaja.
Accessible name biasanya harus merujuk pada judul yang terlihat. Deskripsi teks biasa yang singkat dapat dirujuk secara terpisah. Hilangkan rujukan deskripsi tunggal untuk konten yang berisi beberapa paragraf, daftar, atau tabel agar pengguna teknologi alat bantu dapat menavigasi struktur tersebut. Ikon tutup tetap membutuhkan accessible name, dan kontrol tutup atau batal yang terlihat harus ada bahkan ketika Escape didukung.
Contoh React berikut menjaga status browser native tetap tersinkronisasi dengan status aplikasi terkontrol (controlled state). Prop description sengaja dibuat sebagai string pendek; konten kompleks ditempatkan di children dan tidak ditetapkan sebagai satu deskripsi yang diratakan.
'use client'
import { useEffect, useId, useRef, type ReactNode } from 'react'
interface AccessibleModalProps {
open: boolean
title: string
description?: string
initialFocus: 'heading' | 'cancel' | 'confirm'
children: ReactNode
onConfirm: () => void
onOpenChange: (open: boolean) => void
}
export function AccessibleModal({
open,
title,
description,
initialFocus,
children,
onConfirm,
onOpenChange,
}: AccessibleModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
const headingRef = useRef<HTMLHeadingElement>(null)
const cancelRef = useRef<HTMLButtonElement>(null)
const confirmRef = useRef<HTMLButtonElement>(null)
const titleId = useId()
const descriptionId = useId()
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (open && !dialog.open) {
dialog.showModal()
const target =
initialFocus === 'confirm'
? confirmRef.current
: initialFocus === 'cancel'
? cancelRef.current
: headingRef.current
target?.focus()
} else if (!open && dialog.open) {
dialog.close()
}
}, [initialFocus, open])
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
const handleCancel = (event: Event) => {
event.preventDefault()
onOpenChange(false)
}
const handleClose = () => {
if (open) onOpenChange(false)
}
dialog.addEventListener('cancel', handleCancel)
dialog.addEventListener('close', handleClose)
return () => {
dialog.removeEventListener('cancel', handleCancel)
dialog.removeEventListener('close', handleClose)
}
}, [onOpenChange, open])
return (
<dialog
ref={dialogRef}
aria-labelledby={titleId}
aria-describedby={description ? descriptionId : undefined}
>
<h2 ref={headingRef} id={titleId} tabIndex={-1}>
{title}
</h2>
{description ? <p id={descriptionId}>{description}</p> : null}
{children}
<div>
<button ref={cancelRef} type="button" onClick={() => onOpenChange(false)}>
Cancel
</button>
<button ref={confirmRef} type="button" onClick={onConfirm}>
Confirm
</button>
</div>
</dialog>
)
}Penjaga (guards) di sekitar dialog.open mencegah panggilan imperatif duplikat selama rerender. Listener cancel langsung sangat penting karena event ini dapat dibatalkan (cancelable) dan tidak melakukan bubbling. Mencegah penutupan default memungkinkan controlled state berubah terlebih dahulu; efek berikutnya akan menutup dialog native. Listener close juga memperbaiki status jika jalur penutupan native lainnya berjalan. API browser tetap berada di dalam efek, sehingga server rendering hanya memancarkan markup.
Jaga kebijakan penutupan tetap eksplisit. Klik pada backdrop tidak secara otomatis setara dengan Cancel. Formulir dengan pekerjaan yang belum disimpan dapat mengabaikan klik backdrop, pemilih ringan (picker) dapat menerimanya, dan konfirmasi yang tidak dapat dibatalkan tidak boleh hilang karena pointer event yang tidak disengaja. Jika produk menerima penutupan melalui backdrop, gunakan hit test area backdrop yang teruji dan rutekan melalui jalur penutupan terkontrol yang sama. Pemeriksaan event-target saja dapat salah mengira klik pada padding dialog sebagai klik backdrop. Jangan pernah membuat target penutupan tak terlihat satu layar penuh yang mencuri klik yang ditujukan untuk dialog.
Pemulihan fokus native biasanya kembali ke elemen pemanggil. Alur aplikasi dapat menimpanya hanya dengan alasan yang jelas. Saat dialog membuat baris baru dan menghapus tombol "Add row", sel pertama dari baris baru tersebut adalah tujuan yang logis. Tangkap kebijakan ini di tingkat caller, karena dialog reusable tidak dapat menyimpulkan apa yang berubah dalam alur kerja di sekitarnya.
Untuk dialog bertumpuk, lebih disukai mengubah konten di dalam satu dialog. Jika modal kedua tidak dapat dihindari, pertahankan struktur tumpukan (stack): hanya entri paling atas yang dapat ditutup dari Escape atau backdrop, dialog di latar belakang tetap inert, dan menutup entri paling atas akan memulihkan fokus ke kontrol yang membukanya di dalam dialog sebelumnya. Nilai Boolean global tidak dapat mewakili hubungan tersebut.
Uji perilakunya, bukan hanya atribut yang di-render:
1. Open with Enter and Space; verify focus enters the intended target.
2. Tab from the last control and Shift+Tab from the first; verify background is unreachable.
3. Press Escape; verify one top dialog closes and controlled state becomes false.
4. Use the visible close and Cancel controls with keyboard, pointer, and touch.
5. Close normally; verify focus returns to the opener.
6. Remove the opener during completion; verify focus moves to the chosen logical successor.
7. Read with a screen reader; verify one useful title and no flattened rich description.
8. Open a destructive confirmation; verify initial focus is on the least destructive action.
9. Rerender repeatedly while open; verify no duplicate-open exception or focus reset.
10. Unmount during navigation; verify no listener leak or focus jump to the document body.
11. At 200% zoom and a small viewport, verify title, controls, and scrollable content remain reachable.
12. Run automated accessibility checks, then repeat the manual focus workflow they cannot prove.Contoh Jawaban Berkualitas Tinggi
"Saya akan memperlakukan modal sebagai state machine untuk fokus dan interaksi, bukan sebagai panel dengan nilai stacking yang besar. Saat terbuka, sisa dokumen harus menjadi inert, dialog membutuhkan nama yang terikat pada judul yang terlihat, dan fokus harus berpindah ke target yang dipilih berdasarkan tugasnya. Saat terbuka, navigasi keyboard tetap berada di dalam dan kontrol tutup atau batal yang terlihat selalu dapat dijangkau. Saat ditutup, fokus kembali ke pembuka kecuali alur kerja telah menggantinya, dalam hal ini caller menyediakan penerus logis.
Untuk browser saat ini, saya akan menggunakan elemen dialog native dan memanggil showModal(). Hal itu memberi saya top layer, backdrop, background inertness, dan perilaku fokus modal inti. Menyetel open atau memanggil show() akan menghasilkan perilaku non-modal, jadi saya tidak akan menggunakan keduanya sebagai pengganti. Jika latihan melarang elemen native, saya akan mereproduksi invarian yang sama dengan dialog role, accessible name, inertness luar yang nyata, pengurungan fokus, penanganan Escape, portal, dan pemulihan fokus, sebaiknya melalui primitif teruji yang sudah ada daripada kode focus-trap ad-hoc yang baru.
Initial focus bergantung pada konten. Saya akan memfokuskan field yang relevan untuk formulir pendek, heading statis untuk konten terstruktur yang panjang, dan Cancel untuk tindakan yang tidak dapat dibatalkan. Saya akan merujuk deskripsi singkat hanya jika dapat dipahami sebagai satu pengumuman; daftar dan banyak paragraf tetap dapat dinavigasi sebagai struktur.
Di React, saya akan menyinkronkan controlled state dengan showModal() dan close() di dalam efek, mencegah panggilan duplikat, mendengarkan langsung event cancel yang non-bubbling, dan menghapus listener saat cleanup. Penutupan backdrop akan menjadi kebijakan produk yang eksplisit. Saya akan memverifikasi pembukaan keyboard, kedua arah tab, Escape, penutupan eksplisit, penamaan screen reader, pemulihan fokus saat pemicu ada atau menghilang, rerender berulang, dialog bertumpuk, zoom, dan viewport kecil. Pemeriksaan otomatis melengkapi alur kerja tersebut tetapi tidak menggantikannya."
Kesalahan Umum
- Menata gaya overlay di tengah dan menyebutnya modal → kontrol latar belakang tetap dapat dijangkau oleh keyboard atau teknologi alat bantu → Tentukan dan uji inertness serta siklus hidup fokus.
- Menambahkan semantik modal tanpa memblokir interaksi luar → pohon aksesibilitas menjanjikan status yang tidak dialami oleh pengguna pointer yang dapat melihat → Setel semantik modal hanya jika perilakunya benar-benar modal.
- Melakukan toggle atribut
opennative → dialog ditampilkan tanpa perilakushowModal()→ Gunakan metode modal yang benar dan sinkronkan dengan status aplikasi. - Selalu memfokuskan kontrol pertama → konten panjang dapat dimulai di luar layar dan pekerjaan destruktif dapat memfokuskan tindakan berbahaya → Pilih initial focus dari struktur konten dan konsekuensinya.
- Menempatkan beberapa paragraf ke dalam satu accessible description → screen reader mengumumkan satu blok tanpa struktur → Rujuk hanya deskripsi singkat dan biarkan konten yang kaya tetap dapat dinavigasi.
- Hanya mengandalkan Escape → pengguna layar sentuh dan switch mungkin tidak memiliki jalur penutupan yang jelas → Sertakan kontrol tutup atau batal yang terlihat dan memiliki nama.
- Menutup pada setiap klik backdrop → input pointer yang tidak disengaja membuang pekerjaan → Jadikan light-dismiss sebagai kebijakan produk yang disengaja dan dapat diuji.
- Menulis focus trap manual sebelum memeriksa platform atau pustaka → edge case berlipat ganda di sekitar kontrol yang dinonaktifkan, perubahan DOM, portal, dan dialog bersarang → Lebih pilih elemen native atau primitif matang yang teruji.
- Memanggil
showModal()selama render atau pada setiap eksekusi efek → server rendering gagal atau browser melempar error dan fokus tereset → Panggil metode imperatif dalam efek yang dijaga. - Memulihkan fokus ke pemicu yang sudah dihapus → fokus jatuh ke document body dan konteks keyboard hilang → Minta caller mengidentifikasi penerus logis saat alur kerja mengubah halaman.
- Memperlakukan pemindaian otomatis sebagai bukti lengkap → alat otomatis tidak dapat menilai urutan fokus atau niat alur kerja → Lakukan urutan pengujian keyboard dan screen reader manual secara lengkap.
Pertanyaan Lanjutan dan Tanggapan
Lanjutan 1: Apa yang berubah jika elemen dialog native dilarang?
Render portal yang berisi kontainer ber-role dialog dengan rujukan judul yang terlihat dan semantik modal. Buat setiap root aplikasi latar belakang menjadi inert, bukan hanya diredupkan secara visual. Tangkap pembuka, pilih dan atur initial focus, kurung kedua arah tab saat elemen turunan yang dapat difokuskan berubah, tangani Escape pada dialog yang aktif, pulihkan fokus, dan bersihkan setiap mutasi serta listener. Jelaskan mengapa primitif yang terawat lebih aman daripada mengimplementasikan ulang perilaku lintas browser ini untuk setiap dialog produk.
Lanjutan 2: Bagaimana Anda menangani formulir dengan error validasi?
Biarkan dialog tetap terbuka. Pindahkan fokus ke field pertama yang tidak valid atau ringkasan error yang menautkan ke field yang tidak valid, ekspos setiap pesan melalui accessible description field tersebut, dan pertahankan nilai yang dimasukkan. Jangan umumkan setiap error field sebagai satu deskripsi dialog. Setelah pengiriman berhasil, tutup hanya ketika hasil aplikasi telah di-commit, lalu pindahkan fokus ke elemen yang mewakili hasil tersebut.
Lanjutan 3: Haruskah mengklik backdrop menutup dialog?
Tentukan berdasarkan risiko kehilangan data. Pemilih ringan (picker) dapat mengizinkannya; formulir panjang atau konfirmasi destruktif biasanya memerlukan keputusan eksplisit. Jika diaktifkan, tutup hanya ketika urutan pointer dimulai dan berakhir di area backdrop, gunakan transisi status yang sama dengan Cancel, dan uji pointer down di dalam diikuti oleh pointer up di luar agar aksi drag tidak disalahartikan sebagai niat menutup.
Lanjutan 4: Bagaimana cara menganimasikan penutupan tanpa merusak pemulihan fokus?
Pisahkan "permintaan tutup" dari "dihapus dari DOM". Tandai dialog sebagai sedang menutup, hentikan tindakan baru, jalankan transisi keluar, lalu panggil jalur penutupan native dan perbarui status sebelum unmount. Hormati preferensi reduced-motion dan sediakan batas fallback penyelesaian (bounded completion fallback). Pemulihan fokus terjadi pada batas penutupan sebenarnya, bukan saat opasitas pertama kali berubah.
Lanjutan 5: Bagaimana jika dialog kedua terbuka dari dialog pertama?
Hindari hal ini jika alur multi-langkah dalam satu dialog lebih jelas. Jika diperlukan, simpan tumpukan terurut (ordered stack) dengan pembuka untuk setiap entri. Hanya entri teratas yang merespons Escape atau backdrop; dialog di bawahnya tetap diblokir. Menutup turunan (child) akan memulihkan fokus ke pembukanya di dalam induk (parent), dan menutup induk kemudian memulihkan fokus ke pembuka halaman.
Lanjutan 6: Bagaimana cara mencegah scroll pada body dan layout shift?
Perlakukan penguncian scroll sebagai kebijakan visual dan input yang terpisah dari inertness semantik. Catat posisi scroll saat ini, terapkan satu kunci terpusat saat modal pertama terbuka, kompensasi lebar scrollbar yang menghilang jika diperlukan, dan lepaskan hanya setelah modal terakhir ditutup. Uji keyboard virtual ponsel, area scroll bersarang, zoom, dan perubahan rute; kepemilikan berbasis reference-counted mencegah satu dialog membuka kunci halaman di bawah dialog lainnya.
Lanjutan 7: Bagaimana Anda menguji ini di CI?
Gunakan pengujian komponen untuk transisi buka dan tutup terkontrol, label, percabangan initial-focus, penanganan cancel, dan pembersihan. Tambahkan pengujian browser yang mengaktifkan pemicu sebenarnya, berpindah maju dan mundur melalui fokus, menekan Escape, menghapus pembuka, dan menjalankan skenario dialog bertumpuk. Jalankan aturan aksesibilitas otomatis untuk regresi struktural, lalu pertahankan matriks manual di seluruh konfigurasi perwakilan screen reader, keyboard, zoom, sentuhan, dan kontras tinggi karena asersi CI tidak dapat menilai setiap pengumuman atau pilihan alur kerja.