Navigasi Spasial (Fokus Tombol Arah)

Fitur web: scrollIntoView()

Navigasi fokus terarah (menggunakan tombol panah atau D-Pad) memungkinkan pengguna khusus keyboard dan input alternatif untuk menavigasi item interaktif berdasarkan tata letak visual 2D-nya. Hal ini sangat bermanfaat untuk antarmuka pengguna spasial (seperti media rail, panel dasbor, atau katalog bertingkat), tetapi harus diterapkan dengan hati-hati agar tidak mengganggu perilaku browser native seperti scroll area pandang.

Panduan ini membahas cara membuat navigasi arah 2D yang berperforma tinggi dan mudah diakses di dalam widget gabungan.


Prinsip Arsitektur Inti

Saat menerapkan fokus spasial tombol panah kustom, patuhi aturan dasar berikut:

  1. Batasi untuk widget gabungan: Jangan ganti tombol panah pada alur teks dokumen normal atau tata letak daftar vertikal sederhana yang memerlukan navigasi Tab berurutan default. Lihat Panduan Coding Aksesibilitas untuk mengetahui panduan alur dokumen umum.
  2. Pertahankan scroll batas native: Jika tidak ada elemen yang dapat difokuskan ke arah yang ditekan, jangan cegah perilaku browser default. Biarkan peristiwa menggelembung sehingga browser dapat men-scroll area pandang secara native. Mengganti tombol panah tanpa syarat akan merusak aksesibilitas (WCAG 1.4.10).
  3. Pastikan visibilitas dan perataan fokus: Saat fokus berubah, pastikan elemen yang baru difokuskan di-scroll ke tampilan (misalnya, menggunakan scrollIntoView({ block: 'nearest', inline: 'nearest' })) sehingga tetap terlihat sepenuhnya.
  4. Berkoordinasi dengan sistem tata letak: Tentukan posisi kandidat fokus dari geometri pembatas aktif (getBoundingClientRect()) saat menangani setiap penekanan tombol. Hindari asumsi bahwa urutan DOM atau struktur petak tetap mewakili tata letak visual. Lihat Tata Letak CSS dan Desain Responsif untuk rekomendasi tata letak modern.

Pengelolaan Fokus yang Dapat Diakses

Penampung spasial harus mengekspos satu tab stop ke navigasi Tab Halaman berurutan. Di dalam widget, kelola fokus keyboard menggunakan Pola Tabindex Berpindah:

  • Item aktif memiliki tabindex="0".
  • Semua item tidak aktif memiliki tabindex="-1".
  • Saat menavigasi, pindahkan fokus secara terprogram dengan menggeser atribut tabindex="0" dan memanggil focus().

Pemilihan Kandidat Visual

Dalam tata letak asimetris, digabungkan, atau petak, pemetaan berbasis indeks akan gagal. Sebagai gantinya, dapatkan kandidat dari geometri visual aktif untuk penekanan tombol saat ini:

  • Kandidat: Menggunakan turunan yang dapat difokuskan dari penampung widget yang meneruskan checkVisibility(). Elemen tersembunyi dapat memiliki rect berukuran nol di (0, 0) dan tidak boleh menjadi target up atau left yang tidak disengaja.
  • Geometri: Baca getBoundingClientRect() untuk elemen yang difokuskan dan setiap kandidat saat menangani penekanan tombol. Jangan meng-cache persegi panjang ini secara global: tata letak dapat berubah karena berbagai alasan, dan sejumlah kecil pembacaan geometri tidak memerlukan banyak biaya saat menangani peristiwa utama yang dipicu pengguna.
  • Filter arah: Kandidat berada dalam arah yang ditekan hanya jika tepi dekatnya berada di atau di luar tepi jauh elemen saat ini pada sumbu tersebut, sehingga memungkinkan toleransi kecil untuk pembulatan sub-piksel. Misalnya, kandidat untuk right harus memiliki candidate.left >= current.right - tolerance. Jangan membandingkan pusat untuk menentukan apakah kandidat berada di arah tersebut.
  • Kandidat yang selaras terlebih dahulu: Di antara kandidat yang lulus filter arah, pilih kandidat yang luasnya pada sumbu tegak lurus tumpang-tindih dengan luas elemen saat ini. Pilih celah terkecil pada sumbu navigasi, dengan memutus ikatan dengan offset pusat tegak lurus terkecil. Hal ini membuat navigasi tetap berada di baris atau kolom visual yang berdekatan meskipun itemnya sangat lebar.
  • Skor pengganti: Jika tidak ada kandidat yang tumpang-tindih pada sumbu tegak lurus, beri skor pada kandidat yang tersisa sebagai gap + w * perpendicularCentreOffset dan pilih nilai minimum. Gunakan bobot w >= 2 sebagai titik awal, lalu sesuaikan untuk tata letak; bobot yang lebih besar mendukung kandidat yang lebih selaras.
  • Tidak ada kandidat: Tidak menampilkan apa pun. Pengendali peristiwa tidak boleh memanggil preventDefault(), sehingga browser dapat men-scroll secara native.

Implementasi pastinya dapat bervariasi, tetapi pertahankan pengujian arah berbasis edge dan terapkan preferensi tumpang-tindih sebelum penggantian berbobot. Ini adalah inti dari algoritma; hindari menyajikan cache atau bobot tetap sebagai hal yang benar secara universal.


Integrasi Penekanan Tombol & Kerja Sama Scroll

Router peristiwa Anda harus mencegah scroll area pandang standar hanya saat transisi fokus spasial yang valid terjadi:

  • Jangan mengintersepsi peristiwa saat tombol pengubah ditekan, saat e.defaultPrevented sudah benar, atau saat target peristiwa terdalam menggunakan tombol panah itu sendiri. Gunakan e.composedPath()[0], bukan hanya e.target, karena e.target dapat ditargetkan ulang ke host bayangan. Pemeriksaan kontrol/yang dapat diedit secara praktis adalah target.matches(':read-write, select, input[type="range"], [role="slider"]'); root bayangan tertutup tidak dapat diperiksa.
  • Cakup pemroses ke penampung widget, sehingga beberapa widget spasial dapat berdampingan di satu halaman.
  • Hanya bertindak saat elemen yang difokuskan adalah kandidat itu sendiri. Jika item berisi kontrol yang dapat difokuskan, jadikan kontrol tersebut sebagai kandidat juga atau biarkan saja; jangan selesaikan induk dengan closest().
  • Saat kandidat ditemukan, panggil preventDefault(), perbarui tabindex roving, panggil focus(), dan gunakan scrollIntoView({ block: 'nearest', inline: 'nearest' }).
  • Jika tidak ada kandidat yang ditemukan, tampilkan tanpa memanggil preventDefault() agar scroll native berlanjut.

Hindari menambahkan pintasan satu karakter seperti h/j/k/l atau W/A/S/D untuk navigasi spasial. Tombol panah bukan tombol karakter, dan mempertahankan scroll native saat tidak ada kandidat akan menghindari pemblokiran gerakan area pandang yang tidak perlu.