Satu Komponen Kartu, Dua Grid
Copy class dari kartu jaringan perusahaan ternyata nggak cukup: identitas elemen li dan handler keyboard-nya harus ikut pindah.
Ringkasan
Gara-gara niru kartu pakai copy-paste class Tailwind doang, garis pembatasnya nggak muncul karena struktur DOM-nya beda. Solusinya, kartu kantor diekstrak jadi komponen OfficeCard dengan role button dan handler keyboard, terus tinggal dipakai di dua ul berbeda. Ada juga bug daftar nampil dobel gara-gara ul lama kelupaan dihapus.
Saya buka file office-network.tsx pagi itu dengan satu target sederhana: bikin daftar kantor di bagian bawah tampil persis kayak kartu jaringan perusahaan di bagian atasnya. Yang saya lakuin juga klasik. Blok semua class Tailwind dari kartu yang udah bagus, pindah ke kartu kantor, paste, beres.
Ternyata nggak beres.
Salin Class, Strukturnya Nggak Ikut
Gejalanya pertama kelihatan di garis pembatas antar kartu. Di kartu jaringan perusahaan, garis itu muncul rapi di sisi kiri tiap kartu. Di kartu kantor hasil copy-paste saya, dia nggak nongol sama sekali. Tebakan pertama saya soal typo class. Saya bandingin karakter per karakter, sama persis.
Waktu di-inspect, ketahuan masalahnya. Garis pembatas itu di daratan asalnya nempel ke elemen item daftar yang bawa class reference-divider, dan teknik garisnya memang mangkal dari pasangan daftar-dan-item itu. Kartu kantor saya? Masih div biasa di dalam grid div. Class-nya sama, tapi rumahnya beda.
Di titik ini saya sadar copy class doang nggak akan pernah cukup kalau yang beda itu identitas elemennya. Yang perlu dipindah bukan cuma gaya, tapi struktur.
Jadi li, Tapi Tetap Harus Bisa Diklik
Ubah kartu jadi item daftar punya konsekuensi pertama yang langsung ketahuan: elemen ini nggak boleh nganggur. Menurut MDN, li wajib jadi anak dari daftar tak berurut, daftar berurut, atau menu [1]. Jadi wrapper grid-nya juga harus ikut ganti jadi ul, bukan cuma kartunya.
Konsekuensi kedua lebih halus. Kartu kantor ini interaktif: diklik, peta di atasnya nge-fokus ke lokasi kantor yang bersangkutan. Artinya kartu bawa role="button", tabIndex=0, plus handler keyboard. Kontraknya memang begitu; kalau elemen non-button diberi peran button, dia harus bisa difokuskan dan menangani tombol Enter serta Space lewat handler sendiri [2]. Kalau kamu tab ke kartu, tekan Enter, dan petanya nggak nge-fokus, itu tandanya handler-nya ketinggalan. Semua kelengkapan itu udah ada di kartu lama, dan waktu pindah struktur semuanya wajib ikut, nggak boleh ada yang nyasar.
Ekstraksi Jadi Komponen, Dua Grid Jadi Gampang
Nulis handler keyboard plus struktur list yang sama di beberapa kartu berarti nyalin boilerplate berkali-kali. Solusi yang saya ambil: ekstrak satu kartu jadi komponen sendiri bernama OfficeCard. Ini pola yang sama dengan dokumentasi React soal reuse: komponen diciptakan biar bisa dipakai ulang dan dikomposisi, bukan dikopi-tempel [3]. Dia cuma nerima dua props: office untuk data dan onFocus untuk meneruskan klik ke peta.
function OfficeCard({
office,
onFocus,
}: {
office: OfficeLocation;
onFocus: (name: string) => void;
}) {
return (
<li
role="button"
tabIndex={0}
onClick={() => onFocus(office.name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onFocus(office.name);
}
}}
className="reference-divider ..."
>
{/* icon tile, name, type, address, contact */}
</li>
);
}
// Top row: two cards, centered
<ul className="grid gap-md sm:grid-cols-2">
{content.offices.slice(3).map((office) => (
<OfficeCard key={office.name} office={office} onFocus={focusOffice} />
))}
</ul>
// Bottom row: three cards
<ul className="mt-md grid gap-md sm:grid-cols-2 lg:grid-cols-3">
{content.offices.slice(0, 3).map((office) => (
<OfficeCard key={office.name} office={office} onFocus={focusOffice} />
))}
</ul>
Setelah kartunya mandiri, layout yang tadinya bikin mikir jadi sepele. Kebutuhannya: baris atas isi dua kartu kantor cabang, baris bawah isi tiga kantor utama. Caranya cuma dua ul dengan komposisi kolom beda, sama-sama nge-map ke komponen yang tadi. Baris atas ambil slice(3) dan kolomnya dua; baris bawah ambil slice(0, 3) yang jadi grid tiga kolom di layar lebar. Nambah kantor nanti tinggal geser data, komponen dan grid nggak perlu diutak-atik.
Detail kecil yang ikut kebawa: tile ikonnya pakai size-10 dengan latar aksen transparan dan ring inset, dan tetap aria-hidden karena dekoratif.
Efek angkat pas hover pakai hover:-translate-y-1 tapi digandeng motion-reduce:transform-none, jadi pengguna yang minta kurangi gerakan nggak kena animasinya.
Regresi: Daftar Kantor Muncul Dua Kali
Ini bagian yang bikin saya gregetan. Setelah dipecah jadi dua grid, saya refresh halaman, dan daftar kantornya nampil dobel. Tebakan pertama saya kena bug state React. Cek sana sini, state aman semua.
Penyebabnya jauh lebih bodoh: ul lama yang satu-grid itu belum saya hapus. Dia masih nganggur di bawah pasangan grid baru, nge-render daftar yang sama persis. Satu commit tambahan saya habisin cuma buat hapus beberapa baris JSX yang pertanda nyangkut dari percobaan sebelumnya.
Urutan kerja yang saya pegang sekarang: kalau mau meniru tampilan komponen lain, liat dulu struktur DOM aslinya, jangan cuma class-nya. Dan kalau polanya ternyata dibutuhkan di lebih dari satu tempat, ekstrak jadi komponen sedari awal. Dua langkah kecil itu hemat satu sesi debugging nggak jelas.