Çoğu arayüz hatası görseldir. Bir kısmı ise kullanıcının değiştirmek istemediği sayıları değiştirir ve bu sayılar kaldıraç, emir büyüklüğü veya fiyat ise hata artık görsel değildir.
İşte karşılaştığımız iki tanesi. İkisi de teknik olarak belgelenmiş ama pratikte görünmez olan tarayıcı ve kütüphane davranışından kaynaklanıyor. Düzeltmeler küçük. Onları bulmak küçük değildi.
Önemli Noktalar
- Odaktaki bir <input type="number"> alanının üzerinde tekerleği kaydırdığınızda değeri artar veya azalır. Para alanında bu, değeri sessizce yeniden yazar.
- Bunu preventDefault() ile değil blur() ile çözün: blur çağrısı artışı iptal eder ama sayfa ve modal kaydırmasını tamamen sağlam bırakır.
- document.body'ye portallanmış bir Radix popover, bir Radix dialog içinde odağı tutamaz çünkü dialog'un odak kapsamı odağı geri çeker. Alan canlı görünür ama her tuş vuruşunu yutar.
- Popover'ın portal konteyneri olarak dialog'un kendi elementini verin; böylece popover, tuzaklanan alt ağacın parçası olur.
- Bu düzeltme, elle yazılmış dışarı-tıklama işleyicilerini geçersiz kılar: içerik bir kez portallandığında, ölçüm yaptığınız sarmalayıcının artık bir DOM alt öğesi olmaz.
1. Tekerlek bir düzenleme hareketidir ama kimse kullanıcıya söylemedi
Yerel sayı alanlarının, spesifikasyona kadar uzanan bir davranışı vardır: alan odaktayken, üzerinde fare tekerleğini kaydırmak değeri step özniteliği kadar yukarı veya aşağı adımlar.
Bir alışveriş sepetindeki miktar seçicide bu, hafif bir rahatsızlıktır. Bizim formlarımızda tamamen farklı bir şeydir çünkü söz konusu alanlar kaldıraç, işlem miktarı, kâr payı yüzdesi ve listeleme fiyatıdır. Kullanıcı bir alana tıklar, formun geri kalanını okumak için sayfayı kaydırır ve daha önce belirlediği sayı değişmiştir. Bunu hiçbir şey haber vermez. Form yine de doğrulanır. Bot, kullanıcının asla seçmediği bir değerle başlar.
Naif çözüm, her alana bir wheel işleyicisi eklemek ve preventDefault() çağırmaktır. Bu işe yarar ama kaydırmayı da bozar: imleç bir sayı alanının üzerindeyken kullanıcı artık sayfayı kaydıramaz ve bu, tamamen farklı bir şekilde bozuk hissettirir.
Daha iyi çözüm, spesifikasyondaki ön koşulu kullanır. Artış yalnızca alan odaktayken gerçekleşir, bu yüzden odağı kaldırmak yeterlidir:
const onWheel = (e) => {
const active = document.activeElement;
if (
active instanceof HTMLInputElement &&
active.type === "number" &&
(active === e.target || active.contains(e.target))
) {
active.blur(); // kills the increment, leaves scrolling intact
}
};
document.addEventListener("wheel", onWheel, { passive: true, capture: true });
Bu parçacıktaki üç bilinçli tercih:
preventDefault()yerineblur(). Kaydırmayı iptal etmiyoruz, onu bir düzenlemeye dönüştüren koşulu kaldırıyoruz. Sayfa normal şekilde kaymaya devam eder.capture: true. Dinleyici, olayı iniş yolunda gözlemler, bu yüzden iç bir bileşen yayılmayı durdursa bile yine de tetiklenir.passive: true. Tam dapreventDefault()çağırmadığımız için güvenlidir ve dinleyiciyi kaydırmanın kritik yolundan uzak tutar.
Bir kez bağlanan tek bir global dinleyici, henüz var olmayanlar dahil uygulamadaki her sayı alanını kapsar. Alternatifi, yani her alanı tek tek yamalamak, biri yeni bir form ekleyip unuttuğu anda başarısız olur.
2. İçine yazı yazılamayan, dialog içindeki bir popover
İkincisi, imkânsız bir hata raporu gibi göründü: bir modal içindeki bir açılır menünün içindeki, görünür şekilde odaklanan ama içine yazılan her şeyi yok sayan bir arama kutusu.
Sebep, iki doğru davranışın çakışmasıdır.
Radix popover'lar içeriklerini varsayılan olarak document.body'ye portallar; bu, normal bir sayfada doğrudur çünkü overflow ve yığılma bağlamlarından kaçar. Radix dialog'lar odağı kendi alt ağaçlarının içinde tuzaklar; bu da doğrudur ve bir modalı modal yapan şeydir.
İkisini bir araya getirin: popover'ın içeriği, DOM açısından dialog'un dışındadır. Dialog'un odak kapsamı, gelen her odak olayını kabaca şöyle kontrol eder:
container.contains(target) ? allow : focus(lastFocusedElement)
Arama kutusu dialog tarafından kapsanmadığı için, üzerine düşen her odak olayı anında geri alınıyordu. Alan render oldu, bir an için imleç gösterdi ve bir tuş vuruşu kaydedilmeden önce odağı kaybetti. Canlı görünüyordu ama her şeyi yutuyordu.
Çözüm, popover'ın nerede yaşadığı konusunda yalan söylemeyi bırakmaktır. Radix'in portalı bir container kabul eder, bu yüzden dialog'un içine boş, sıfır yükseklikte bir div render ederiz ve popover'ı onun içine portallarız:
// inside the dialog
<div ref={setDropdownHost} />
...
<PopoverContent container={dropdownHost}>…</PopoverContent>
Yanlış yapmanın kolay olduğu iki ayrıntı:
- Ref değil, state. Ana düğüm ilk render'da yoktur ve var olduğunda portalın bir yeniden render'a ihtiyacı vardır. Bir
useRefdüğümü tutardı ama o render'ı asla tetiklemezdi. - Ana düğüm yerleşimi etkilememelidir. Boş ve sıfır yüksekliktedir, bu yüzden hiçbir şeyi kaydırmadan dialog'un işaretlemesinde uygun herhangi bir yere yerleşebilir.
3. Yanındaki kodu sessizce bozan düzeltme
Bu, en çok vurgulayacağım kısım çünkü temiz bir düzeltmenin bir regresyona dönüştüğü yer burasıdır.
Portallanmış bir popover'a geçmeden önce, açılır menünün elle yazılmış bir dışarı-tıklama işleyicisi vardı: sarmalayıcıda bir ref tut, belge tıklamalarını dinle, tıklama içeride değilse kapat.
İçerik bir kez portallandığında, görsel olarak onun içinde olsa da artık o sarmalayıcının bir DOM alt öğesi değildir. Bu yüzden işleyici, artık açılır menünün kendi arama kutusuna yapılan tıklamaları dışarıya yapılmış tıklamalar olarak görür ve kullanıcının kullanmaya çalıştığı şeyi kapatır.
O işleyiciyi kaldırmak bir temizlik değildi, düzeltmenin bir parçasıydı. Genel kural:
Portallama, DOM kapsamına dayalı her varsayımı bozar. Dışarı-tıklama işleyicileri,
closest()aramaları, olay delegasyonu ve CSS alt öğe seçicileri, hepsi sessizce anlam değiştirir. Bir şeyi portalladığınızda, kapsam varsaymış olan kodu bulmaya gidin.
Bu ikisinin ortak yanı
Her iki hata da “kütüphane doğru davranıyor” ile “kullanıcı kötü bir deneyim yaşıyor” arasındaki boşlukta yer alır. Hiçbir şey hata fırlatmadı. Hiçbir şey loglanmadı. Her iki durumda da bileşen, ekran görüntüsünde doğru görünüyordu.
Aklımda tutacağım genel dersler:
- Platformun hangi hareketleri düzenleme olarak ele aldığını bilin. Odaktaki bir sayı alanının üzerindeki tekerlek bunlardan biridir; Linux'ta orta tık ile yapıştırma da öyle. Bir alan para tutuyorsa, bunları denetleyin.
- Bir olayı iptal etmek yerine bir ön koşulu kaldırmayı tercih edin.
blur(),preventDefault()'a üstün geldi çünkü belirtiyi bastırmak yerine sebebi hedef aldı ve ilgisiz davranışa dokunmadı. - Başarısızlık modu “biri unutur” olduğunda, bir kez ve global olarak çözün. Alan başına yapılan bir düzeltme, son kullanma tarihi olan bir düzeltmedir.
- Portalladığınızda, kapsam varsayımlarını denetleyin. DOM ağacı ile görsel ağaç birbiriyle uyuşmayı bırakır ve ikisini birbirine karıştıran her kod artık yanlıştır.
