- Tombol More di situs web BBC UK gagal menangani klik hanya pada lingkungan kerja dari rumah tertentu; bug UI yang tampak biasa ternyata merupakan masalah sistem koordinat multi-monitor
- Jika monitor eksternal ditempatkan di atas·kiri monitor utama, nilai
screenX dan screenY pada event click di Chrome dan Firefox bisa menjadi negatif
- Kode lama menentukan klik pointer dengan
event.screenX > 0 || event.screenY > 0, sehingga klik dengan koordinat negatif tidak dianggap sebagai klik mouse
- Perbaikannya sederhana: bukan memeriksa apakah
screenX dan screenY lebih besar dari 0, melainkan apakah keduanya bukan 0, menjadi bentuk event.type === 'click' && (event.screenX!== 0 || event.screenY!== 0)
- Meski sudah melewati unit test, Puppeteer, pengujian manual, dan pengujian teknologi bantu, bug seperti ini tetap bisa tersisa karena ambiguitas spesifikasi UI Events dan asumsi koordinat multi-monitor
Bug navigasi BBC yang hanya tereproduksi di lingkungan tertentu
- Bilah navigasi di situs web BBC UK membuka menu ketika pengguna mengaktifkan tombol More
- Tombol ini menggunakan event
click, dan event tersebut dapat dipicu bukan hanya oleh mouse, tetapi juga oleh sentuhan serta tombol Enter dan Space pada keyboard
- Seorang anggota tim mengalami masalah hanya saat menggunakan laptop kerja di rumah; laptop yang sama berfungsi normal ketika digunakan di kantor
- Di rumah pun, kegagalan hanya terjadi ketika jendela browser berada di monitor eksternal; di layar laptop, tombol berfungsi normal
- Ketika masalah terjadi, alih-alih handler JavaScript membuka menu, menu terbuka melalui perilaku fallback tanpa JavaScript
- Masalah yang sama tidak muncul di Safari
Kondisi reproduksi adalah posisi monitor
- Tim mempersempit kondisi reproduksi dengan memeriksa elemen apa di lingkungan rumah yang memicu masalah
- Monitor eksternal ditempatkan di atas layar laptop, dan masalah berhenti ketika penempatan ini diubah di pengaturan OS
- Anggota tim lain juga dapat mereproduksi bug dengan menyamakan penempatan monitor di OS dengan cara yang sama
- Dua kondisi yang teridentifikasi di awal investigasi adalah:
- Masalah tidak terjadi di Safari
- Masalah terjadi ketika monitor eksternal berada di atas dan kiri monitor utama
Koordinat negatif pada screenX, screenY
- Saat event
click pada tombol More diperiksa dengan console.log, nilai screenX dan screenY di Chrome dan Firefox muncul sebagai negatif
- Event
click, apa pun input yang memicunya, adalah salah satu jenis PointerEvent, sehingga objek event memuat informasi mouse atau pointer sentuh yang memicu klik
screenX dan screenY menunjukkan koordinat titik yang diklik pada layar dalam satuan piksel
- Dalam spesifikasi DOM UI Events, tidak terlihat informasi spesifik tentang apakah properti tersebut dapat bernilai negatif
- Perbedaan antara Safari dan Chrome·Firefox menunjukkan bahwa cara browser merepresentasikan koordinat layar dalam konfigurasi multi-monitor bisa berbeda
- Masalah interoperabilitas ini telah dilaporkan ke tim WebKit
Perbedaan cara browser menangani koordinat multi-monitor
- Dalam konfigurasi multi-monitor, sistem koordinat layar browser memperlakukan beberapa monitor seperti satu layar besar
- Jika ada dua monitor 800px yang ditempatkan secara horizontal, rentang koordinat x bisa menjadi 0 hingga 1600
- Di Safari, rentang koordinat tampaknya selalu berupa rentang positif yang dimulai dari monitor paling kiri atas
- Di Chrome dan Firefox, koordinat tampaknya dihitung relatif terhadap monitor utama, sehingga layar yang berada di atas atau kiri monitor utama dapat menghasilkan koordinat negatif
- Bug ini hanya terjadi ketika
screenX dan screenY bernilai negatif
Kode bermasalah dan perbaikannya
isInvokedByMouse pada kode bermasalah mencoba memastikan apakah event click dipicu oleh mouse atau pointer sentuh dengan memeriksa apakah screenX dan screenY bernilai positif
const isInvokedByMouse = event => event.screenX > 0 || event.screenY > 0;
const isInvokedByKeyboard = event => isEnterKey(event) || isSpaceKey(event);
// ...
const toggleMenu = event => {
// ...
if (isInvokedByMouse(event) || isInvokedByKeyboard(event)) {
event.preventDefault();
// Do stuff to open the menu and move the focus...
}
};
- Kode ini mengasumsikan bahwa
screenX dan screenY dari event click yang dipicu pointer akan berupa bilangan positif
- Ketika pengguna mengklik tombol
More pada monitor dengan koordinat layar negatif, event handler tidak mengakui klik tersebut, lalu jatuh ke perilaku default tautan More
- Perbaikannya adalah memeriksa apakah
screenX dan screenY bukan 0, alih-alih apakah lebih besar dari 0
const isInvokedByMouse = event =>
event.type === 'click' && (event.screenX !== 0 || event.screenY !== 0);
- Dengan perubahan ini, pengguna dengan penempatan multi-monitor yang tidak umum pun dapat menggunakan bilah navigasi situs web BBC
Masalah desain yang tersisa dan refactoring lanjutan
- Perbaikannya sendiri sederhana, tetapi masih ada bagian yang janggal dalam kode
- Tidak perlu memeriksa apakah
click dipicu oleh mouse atau keyboard, dan event handler menjadi rumit karena juga menangani event keydown
- Perlu berhati-hati terhadap asumsi yang dibuat tentang perilaku API, dan ketidakjelasan spesifikasi tentang apakah
screenX dan screenY bisa bernilai negatif juga menyembunyikan masalah ini
- Kode ini telah melewati unit test, pengujian Puppeteer, serta pengujian manual menggunakan berbagai browser, perangkat, dan alat teknologi bantu, tetapi bugnya tidak ditemukan
- Dalam pembaruan pada 19 November 2024, komponen navigasi kemudian telah di-refactor dan event handler tombol
menu juga berubah besar
- Tulisan lanjutan menjawab cara refactoring dan pertanyaan yang sering muncul: How I refactored the BBC navigation bar and a follow-up FAQ
1 komentar
Komentar Hacker News
Sebagai tambahan bagi yang belum sampai mengeklik laporan bug WebKit: seorang pengembang WebKit bertanya kepada BBC mengapa akan berguna jika bisa mendeteksi apakah sebuah event berasal dari keyboard, dan penulis menjawab bahwa interoperabilitas diperlukan karena kasus penggunaan terkait aksesibilitas
Tombol menu pada navigation bar situs BBC UK berperilaku sedikit berbeda saat dibuka dengan pointer dan saat dibuka dengan keyboard. Event click selalu membuka menu, tetapi jika dibuka dengan pointer, fokus berpindah ke kontainer menu; jika dibuka dengan keyboard, fokus berpindah ke tautan pertama dalam menu tanpa animasi pembukaan menu. Event
clickbersifat independen dari perangkat, sehingga bagus untuk membuat pengalaman pengguna keyboard, dan pada keyboard hanya dipanggil dengan Space atau Enter. Jika memakaikeydown, kita harus memeriksa sendiri apakah itu Space/EnterSumber: https://bugs.webkit.org/show_bug.cgi?id=281430
const isInvokedByMouse = event => event.screenX > 0 || event.screenY > 0;danconst isInvokedByKeyboard = event => isEnterKey(event) || isSpaceKey(event);; sekilas tampak seperti mencoba mengklasifikasikan event sebagai salah satu dari mouse atau keyboardPada kenyataannya, muncul empat kategori: mouse tetapi bukan keyboard, keyboard tetapi bukan mouse, keduanya, dan bukan keduanya. Seperti bug aslinya, “bukan keduanya” ditangani dengan tidak semestinya, dan saya juga ragu apakah “keduanya” bekerja dengan benar. Kode tersebut seharusnya secara sengaja menangani fakta bahwa status keyboard dan status mouse adalah boolean terpisah, atau disusun agar
eventSourcemengembalikan kategori yang saling eksklusif seperti"keyboard","mouse","not sure"Lebih baik mengikuti perilaku default dan merancang komponen yang bekerja untuk kedua kasus penggunaan. Dalam aksesibilitas, jangan mencoba terlalu pintar. Pada akhirnya solusinya menjadi hampir seperti hack, dan pendekatan seperti itu pasti akan pecah atau menimbulkan efek samping. Alasan jarang ada handle yang bagus untuk memperlakukan sesuatu secara berbeda dalam konteks aksesibilitas adalah karena sejak awal area itu memang tidak dimaksudkan untuk diperlakukan berbeda
Pada saat yang sama, mereka juga menginginkan kenyamanan untuk hanya binding ke satu event.
clickmemungkinkan hal ini, tetapi karena tidak ada cara untuk mengetahui apakah event terjadi karena klik mouse atau input keyboard, di Chrome mereka memakai heuristic yang rapuh: jika posisi mousescreenX=0,screenY=0, itu dianggap sebagai klik di titik asal atau trigger keyboard. Dari pengalaman mengerjakan proyek aksesibilitas, ini ide yang cukup buruk, dan kalau saya melihatnya di PR, saya akan meminta ditulis ulang. Idealnya memang browser berperilaku sama, tetapi masalah sebenarnya tampaknya adalah bahwa padaclickyang dipicu keyboard,screenXdanscreenYhampir tidak bermaknaIdealnya, alih-alih mengeluarkan
MouseEvent, seharusnya ada event yang lebih umum yang berlaku untuk keyboard maupun mouse, misalnya semacam"trigger", dan menyediakan informasi sumber trigger. Karena saat ini belum ada di spesifikasi dan butuh solusi segera, jauh lebih stabil dan tidak terlalu hacky untuk juga binding kekeydown, lalu jikaclickterjadi pada elemen yang sama bersamakeydown, anggap itu sebagai input keyboardscreenXdanscreenY, tetapi saya masih bertanya-tanya mengapascreenXharus mengembalikan koordinat layar nyata, bukan posisi internal renderer atau posisi halaman yang dirender sepertilayerX,layerYKebutuhan penulis bisa dipenuhi dengan posisi renderer, tanpa harus membocorkan posisi jendela browser ke setiap situs web yang dikunjungi
don’tmungkin salah ketik yang membuat maknanya menjadi kebalikan dari maksudnyaPada bagian yang mengatakan “cukup mengubah
isInvokedByMousedari memeriksa apakahscreenXdanscreenYlebih besar dari 0 menjadi memeriksa apakah keduanya tidak sama dengan 0”, saya penasaran apa yang terjadi jika, meski sangat jarang, pengguna benar-benar mengklik mouse di posisi 0,0Saya tidak terlalu akrab dengan JS; apakah pemeriksaan
!= 0benar-benar cara terbaik atau satu-satunya? Setelah membaca ulang, sepertinya kalimat bahwa event handler juga menanganikeydownsehingga rumit dan perlu refactor lebih lanjut nanti, tetapi untuk saat ini perbaikan ini sudah cukup, sedikit banyak menjawab bagian iniinstanceof MouseEvent, tetapi itu juga terasa berisiko atau seperti hackSaya bertanya-tanya mengapa mereka bergantung pada heuristic seperti ini. Mungkin karena
toggleMenudipakai di beberapa event handler, atau mungkin ada alasan lain yang spesifik pada codebase tersebut. Sulit menilai tanpa melihat gambaran lengkapnya. Sepertinya jawabannya ada di sini: https://news.ycombinator.com/item?id=42174177event.name == 'click'. Kalau begitu, saya tidak mengerti mengapa mereka ingin memfilter sebagian event click yang validDulu saya pernah memakainya untuk memilih layout mana yang akan ditampilkan. Jika hanya ingin mendengarkan input sentuh, lakukan itu lalu panggil
preventDefaultpada event agar browser tidak membuat eventclicksetelahnya. Atau, cukup kurangi repotnya dan tulis click handler sajaPatut diapresiasi bahwa BBC berinvestasi pada aksesibilitas lalu menemukan bug yang tidak menyenangkan ini. Namun mengapa industri masih belum bisa membuat dropdown yang terbuka secara konsisten untuk semua pengguna?
Apakah aksesibilitas memang sesulit itu? Apakah BBC seharusnya memakai web framework atau web component yang sudah menangani hal seperti ini? Sebagai developer full-stack yang berfokus pada backend, saya berhati-hati saat menyentuh komponen browser. Ada banyak detail halus dalam perilakunya, dan implementasinya sudah teruji dalam waktu lama. Misalnya, membuat text box kustom tanpa meneliti secara mendalam perilaku text box di tiap platform tampak mudah gagal. Bahkan di situs perusahaan besar pun saya sering melihat copy/paste rusak dan karakter hilang. Saya tidak paham mengapa text box masih rusak pada 2024, dan sekarang React terasa arogan
Secara pribadi, saya akan menanganinya dengan template sisi server, CSS framework seperti Bulma, dan JS seminimal mungkin. Ini tidak cocok untuk situs yang menuntut custom branding yang mulus, tetapi text box berfungsi dengan baik dan biaya pengembangannya juga tidak berlebihan. Saya tidak yakin apakah itu memenuhi standar aksesibilitas BBC
Contoh nyatanya adalah modal. Jika Anda tidak memiliki gangguan penglihatan, Anda bisa melihat ada kotak putih di atas area abu-abu “jangan disentuh”, dengan komponen UI mengambang di dalamnya. Jika memakai screen reader, tidak ada jaminan Anda menerima informasi itu. Saat berpindah antar elemen UI dengan tab lalu kembali ke bagian paling atas kotak, apakah screen reader tertentu akan memberi tahu hal itu? Apakah ia akan mencantumkan elemen interaktif yang tersedia? Apakah urutannya sama seperti screen reader lain? Bagaimana di ponsel, bagaimana di Mac? Apakah screen reader dan browser akan melaporkan elemen input dengan benar, atau diam-diam membiarkan pengguna keluar dari modal dan kembali ke area situs lainnya?
Dalam aksesibilitas, kita tidak bisa percaya bahwa sistem operasi, browser, dan screen reader akan bekerja sama atau bertindak masuk akal pada situasi yang tepat. Pada 2019, saya harus melaporkan bug di VoiceOver + Safari yang membuat screen reader membaca blok teks RTL dengan urutan terbalik karena CSS margin bernilai negatif. Secara visual terlihat sebagai
9/10/2019, tetapi di screen reader terdengar seperti “ten slash nine slash two-thousand-and-nineteen”, dan sebagai solusi sementara kami harus memberi teks ituaria-hidden, lalu memasukkan tagptak terlihat dengan urutan yang benar. Jadi ketika melihat kode aneh terkait aksesibilitas, kadang memang benar-benar tidak ada cara yang lebih baik. Bahkan jika Anda membongkar total codebase dan menjadikan aksesibilitas prioritas utama, semuanya bisa rusak dengan cara yang sulit dipahami begitu JAWS atau VoiceOver diperbaruiSecara umum tidak apa-apa, tetapi ada alasan file
reset.cssada, dan dalam kasus ini tampaknya mereka mungkin memakai pendekatan yang lebih ekstrem untuk sepenuhnya menghindari masalah seperti ini. Saya sedang mencoba menebak alasan di balik keputusan merekaIni tampak seperti bug yang dibuat sendiri akibat heuristik yang keliru. Mereka mengasumsikan nilai
screenX/Ypositif berarti event mouse, dan kurangnya pelacakan/logging membuat investigasi menjadi lebih rumitAgak mengejutkan bahwa alih-alih memeriksa properti yang lebih tepat seperti
pointerTypeyang disarankan komentar lain, solusi penulis justru menambahkan heuristik yang rapuh lagi. Seolah dari dua petunjuk terakhir disimpulkan bahwa saat memeriksa koordinatscreenXdanscreenY, mereka harus memeriksa nilai negatif juga, bukan hanya positifpointerId === -1, lalu fallback kescreenX === 0Sekitar 4 tahun lalu, saat kode ini pertama kali ditulis, belum semua browser menggunakan PointerEvent untuk
clickSaya bahkan tidak paham mengapa sejak awal sebuah situs web bisa memperoleh posisi mouse dalam sistem koordinat layar
window.screenX/window.screenY, dan posisi klik juga bisa dilaporkan dalam sistem koordinat itu, terdengar tidak masuk akal di desktopTOR Browser tampaknya menyamarkan
screenXdanscreenYuntuk menghindari fingerprinting. Saya penasaran apakah ada yang pernah melihat use case yang bagus untuk fitur ini. Yang terpikir hanya aplikasi dua jendela yang saling berinteraksi, atau situs yang perilakunya berubah tergantung posisi di layar virtualContoh: https://youtu.be/3al8prbfK5o?si=loNtyqIfMFkppm5V
Saya tidak mengerti mengapa mereka memeriksa koordinat alih-alih
event.type. Namun artikelnya sendiri adalah teka-teki yang bagus, dan saya bisa memahami situasi ketika melihat kode yang tidak saya tulis lalu bertanya, “mengapa penting koordinat klik tidak boleh 0?”, “tidak bisakah cukup periksa apakahevent.targetadalah tombol yang ingin diaktifkan?”, “mengapa memakai JavaScript padahal hal yang sama bisa dilakukan dengan tagdetails/summary?”Mengapa sejak awal memfilter berdasarkan koordinat layar? Bagaimana jika pengguna memakai perangkat input alternatif yang tidak memiliki layar?
Event
clicksaja sudah menjadi sinyal yang cukup bahwa pengguna mencoba mengaktifkan menu. Saya tidak paham mengapa perlu menciptakan ulang rodaisInvokedByMousememeriksa apakah koordinatscreenXatauscreenYbernilai positif untuk memastikan eventclickdipanggil oleh mouse atau pointer sentuh, bukan keyboardMereka mencoba mendeteksi apakah itu aktivasi keyboard atau mouse, dan penulis tampaknya berasumsi bahwa koordinat layar dari event mouse akan selalu positif
Saya mengunggah satu tulisan blog lagi untuk menjelaskan konteks yang membuat orang penasaran dan menjawab pertanyaan. Di sana saya menjelaskan mengapa sejak awal memeriksa
screenX === 0, mengapa saya menginginkan perilaku berbeda tergantung input keyboard dan mouse, serta bagaimana saya melakukan refaktor untuk mencegah insiden tambahanSemoga bermanfaat: https://www.joshtumath.uk/posts/2024-11-18-how-i-refactored-...
Apa cara yang benar untuk memastikan apakah itu klik mouse atau klik keyboard? Rasanya ingin menetapkan flag tingkat modul berdasarkan event yang paling baru terjadi: jika
mousedownlebih baru, setisKeyboard=false,isMouse=true; jikakeydownlebih baru, set sebaliknyaDengan begitu fungsi
isInvokedByMousedanisInvokedByKeyboardtidak diperlukan. Adakah cara yang lebih baik? Mengandalkan koordinat layar untuk ini terasa sangat mencurigakan dan seperti hackevent.detail[1] bernilai 0 pada “klik” keyboard dan 1 pada klik pointer1: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/det...
Sangat menarik, tetapi saya tidak mengerti mengapa browser melaporkan koordinat yang berbeda tergantung monitor. Saya kira browser memperlakukan halaman web seolah-olah selalu berada di satu layar penuh, di display mana pun halaman itu berada
Apakah ada alasan Web API perlu memiliki informasi seperti ini? Ini terlihat seperti risiko keamanan, kebocoran informasi, dan pelacakan
Bukankah ini masalah kemampuan pengembangan? Seharusnya yang dipakai bukan koordinat layar, melainkan koordinat viewport, dan dibaca dengan
.clientXserta.clientY. Saya tidak paham mengapa nilai negatif di ruang layar dianggap bughttps://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_view/...