İçerik Akışı İçin Kararlı Arayüzler Tasarlama
Twitter'da paylaş, LinkedIn
Akışlı kullanıcı arayüzleri görünüşte kolay bir kavramdır ancak pratikte oldukça karmaşıktır. Düzen değişimleri ve hareket tercihlerinden uygun işaretlemeye ve çeşitli durumlara kadar, anında belli olmayabilecek, hesaba katılması gereken birçok husus vardır. Yayın kesilirse ne olur? Kullanıcılar klavyedeki kullanıcı arayüzü değiştikçe sekmeler arasında gezinebilir mi? Hangi ARIA özelliklerine ihtiyaç duyulabilir? Bu makalede ele alacağımız türden şeyler bunlar. Akışlı kullanıcı arayüzleri görünüşte kolay bir kavramdır ancak pratikte oldukça karmaşıktır. Düzen değişimleri ve hareket tercihlerinden uygun işaretlemeye ve çeşitli durumlara kadar, anında belli olmayabilecek, hesaba katılması gereken birçok husus vardır. Yayın kesilirse ne olur? Kullanıcılar klavyedeki kullanıcı arayüzü değiştikçe sekmeler arasında gezinebilir mi? Hangi ARIA özelliklerine ihtiyaç duyulabilir? Bu makalede ele alacağımız türden şeyler bunlar.
Yanıt hâlâ oluşturulurken artık daha fazla arayüz işleniyor. Kullanıcı arayüzü bir durumda başlar ve daha fazla veri geldikçe güncellenir. Bunu sohbet uygulamalarında, günlüklerde, transkripsiyon araçlarında ve diğer gerçek zamanlı sistemlerde görürsünüz.
Sistem Güvenliği
İşin zor kısmı arayüzün sabit bir durumda olmaması; yeni içerikler geldikçe değişmeye devam ediyor. Çizgilerin uzadığı ve yeni blokların ortaya çıktığı yerde büyüyor. Ekranın hemen altındaki bir şey aniden hareket edebilir ve kullanıcının kaydırma konumunu yönetmek zorlaşır. Kullanıcı zaten etkileşimdeyken kullanıcı arayüzünün bazı kısımları eksik bile olabilir.
Detaylar ve Etkileri
Bu yazıda basit bir arayüz alıp bu işi doğru şekilde halletmesini sağlayacağız. Okuma deneyimini bozmadan işleri nasıl sabit tutacağımıza, kaydırmayı nasıl yöneteceğimize ve kısmi içeriği nasıl oluşturacağımıza bakacağız.
Bir Akış Kullanıcı Arayüzü Aslında Neye benziyor?
Gelecekte Ne Bekleniyor?
İçeriği farklı şekillerde yayınlayan üç demo oluşturdum: bir sohbet balonu, bir günlük akışı ve bir transkripsiyon görünümü. Görünüşte farklı görünüyorlar ama hepsi aynı üç sorunla karşılaşıyor.
Birincisi kaydırmadır. İçerik akışı sırasında çoğu arayüz, görünüm penceresini alt tarafa sabitlenmiş halde tutar. Sadece izliyorsanız bu işe yarar, ancak bir şey okumak için yukarı kaydırdığınızda sayfa tekrar kapanır. Bunu sen istemedin. Arayüz sizin yerinize karar verdi ve artık okumak yerine onunla savaşıyorsunuz.
Teknik Analiz
İkincisi ise düzen değişikliğidir. İçerik akışı, kapsayıcıların sürekli olarak büyüdüğü ve büyüdükçe aşağıdaki her şeyin aşağıya doğru kaydığı anlamına gelir. Tıklamak üzere olduğunuz bir düğme artık olduğu yerde değil. Okuduğunuz bir satır taşındı. Sayfa bozuk değil; sadece hiçbir şey rahatça etkileşime girebilecek kadar uzun süre hareketsiz kalmıyor.
Sonuç ve Değerlendirme
Üçüncüsü ise render sıklığıdır. Tarayıcılar ekranı saniyede yaklaşık 60 kez boyar ancak akışlar bundan çok daha hızlı ulaşabilir. Bu, tarayıcının sayfadaki her şeyin dahili temsili olan DOM'un, kullanıcının gerçekte asla göremeyeceği çerçeveler için güncellendiği anlamına gelir. Her güncellemenin hâlâ bir maliyeti vardır ve bu maliyet, performans düşmeye başlayıncaya kadar sessizce artar.
Uzmanların Görüşleri
Her demoyu incelerken işlerin nereden başladığına dikkat edin. Arayüzün yolunuza çıkmaya başladığı o küçük sürtünme anı. Tam olarak bunu düzeltmek için buradayız.
Örnek 1: Yapay Zeka Sohbet Yanıtlarının Akışı
Önemli Gelişmeler
Bu en bilinen durumdur. Akış'ı tıklattığınızda mesaj, tıpkı tipik bir AI sohbet arayüzü gibi, simge bazında büyümeye başlar.
İşte denemenizi istediğim şey:
Nasıl Önlem Alınmalı?
Akış düğmesini tıklayın.
düğme. Mesaj akışı sırasında yukarı kaydırmayı deneyin.
Hızı artırın (10ms gibi bir şeye).
İnce ama önemli bir şeyi fark edeceksiniz: Kullanıcı arayüzü sizi aşağı çekmeye çalışıyor. Temel olarak dikkatinizin nerede olması gerektiğine sizin adınıza karar vermektir.
Bu bir örnek. Bir başkasına bakalım.
Örnek 2: Günlük Görüntüleyicide Canlı İşleme
Bu örnek ilk bakışta farklı görünüyor ancak sorun aslında ilk örneğe çok benziyor. Zamanla uzayan bir mesaj yerine, terminal veya günlük akışı gibi sürekli olarak yeni satırlar eklenir.
Buradaki ilginç kısım kuyruk geçişidir. Etkileşim ve kararlı arayüzler arasındaki dengeyi çok net bir şekilde ortaya koyuyor:
Yine denemenizi istediğim şey şu:
Başlat düğmesine tıklayın.
düğme. Günlüklerin konteynerin yüksekliğini geçmesine izin verin.
Başa doğru ilerleyin.
Akışı durdurun ve "kuyruk" seçeneğini devre dışı bırakın.
Şuna dikkat edin, tai l etkinleştirildiğinde kullanıcı arayüzü yeni içeriği takip eder. Ancak yukarı kaydırıp yerinde kalamazsınız. Bunun yerine, içeriği keşfetmek için akışı durdurmanız veya "kuyruk"u etkinleştirmeniz gerekir.
Örnek 3: Gerçek Zamanlı Metrikleri Görüntüleyen Kontrol Paneli
Bu durumda kullanıcı arayüzü güncellenir:
Değerler sürekli olarak yenilenir.
Bu sefer kaydırma gerginliği yok ancak farklı bir sorun ortaya çıkıyor. Bundan sonra ele alacağımız şey bu.
Kullanıcı Arayüzü Neden Kararsız Hissediyor ve Nasıl Düzeltilir?
Sohbet demosunu denediyseniz ve yanıtlar gelirken ekranı yukarı kaydırdıysanız, ilk sorunu hemen fark etmiş olabilirsiniz: Kullanıcı arayüzü, güncellenirken sizi en son akışlı içeriğe geri çekmeye devam ediyor. Bu sizi bağlamın dışına çıkarır ve içeriği geçtikten sonra tamamen sindirmenize asla zaman tanımaz.
Aynı sorunu ikinci örnekte, günlük görüntüleyicide de görüyoruz. Kuyruk geçişi olmadan, akışlı içerik kaydırma konumunuzu geçersiz kılar.
Bunlar, kod hataları ürettikleri geleneksel anlamda hatalar değildir; daha ziyade tüm kullanıcıları etkileyen erişilebilirlik sorunlarıdır. Bununla birlikte, çalışmanızı planlayıp test ederken dikkatli kullanıcı deneyimi değerlendirmeleri ile bu sorunlar giderilebilir ve önlenebilir.
Tahmin Edilebilir Kaydırma Davranışını Sağlayın
Kullanıcının akışın alt kısmında olduğunu algıladığında otomatik kaydırmayı etkinleştirin.
Kullanıcı yukarı kaydırdığında otomatik kaydırmayı durdurun.
Kullanıcı akışın en altına geri dönerse otomatik kaydırmayı sürdürün.
Bunu yapmak için, kullanıcının kasıtlı olarak alttan uzaklaşıp uzaklaşmadığını bilmemiz gerekir; kaydırma konumu manuel olarak değiştirildiğinde bunun doğru olduğunu varsayabiliriz. Bu davranışı bir bayrakla takip edebiliriz.
let userScrolled = false; chatEl.addEventListener('scroll', () => { const boşluk = chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight; userScrolled = boşluk > 60; });
Bu 60 piksel eşiği önemlidir. Bu olmadan, küçük düzen değişiklikleri (yeni bir satır gibi), kullanıcı gerçekten kaydırmamış olsa bile kısa süreliğine bir boşluk yaratacak ve otomatik kaydırmayı bozacaktır.
Şimdi otomatik kaydırmayı yalnızca kullanıcının kaydırma konumu akışın kaydırma yüksekliğine eşit olduğunda, yani kullanıcı akışın alt kısmında olduğunda etkinleştirdiğimizden emin olalım:
function autoScroll() { if (!userScrolled) { chatEl.scrollTop = chatEl.scrollHeight; } }
Gözden kaçması kolay küçük bir şey: yeni bir yayın başladığında userScrolled'ı sıfırlamamız gerekiyor. Aksi takdirde, önceki mesajdaki bir kaydırma, bir sonraki mesaj için otomatik kaydırmayı sessizce devre dışı bırakabilir.
Düzen Kararlılığını Sağlamlaştırın
Bunu ilk örnekte de gördük. Yeni içerik akışı başladıkça düzen atlar veya kayar ve sizi mevcut bağlamınızın dışına çıkarır. Neyin değiştiği konusunda daha spesifik olmak gerekirse: bu, geniş anlamda sayfa düzeni değil, doğrudan sohbet balonunun altındaki içeriktir.
Koda bakmadan önce belirtmeye değer daha incelikli bir yapı daha var: imleç titremesi. İnnerHTML'yi sildiğimiz ve her tıklamada her öğeyi yeniden oluşturduğumuz için, imleç yüksek hızlarda saniyede 80 defaya kadar sürekli olarak yok ediliyor ve yeniden ekleniyor.
Normal hızda gözden kaçırmak kolaydır, ancak kaydırıcıyı yaklaşık 30 ms'ye kadar yavaşlatırsanız metnin sonunda hafif ama kalıcı bir titreme göreceksiniz. Yeniden oluşturma modelini düzelttiğimizde titreme tamamen kaybolur.
Bu yeniden inşa modeli tam burada; gelen her karakterde çalışan şey budur:
bubble.innerHTML = ''; fullText.split('
').forEach(line => { const p = document.createElement('p'); p.textContent = line || '\u00A0'; bubble.appendChild(p); }); bubble.appendChild(imleçEl);
Bu işe yarıyor ama pahalı. Her güncelleme DOM'u siler ve yeniden oluşturur, her seferinde düzenin yeniden hesaplanmasını zorlar.
Şimdi doğrudan canlı bir düğüme yazıyoruz:
geçerliP = null olsun; function initBubble(kabarcık, imleç) { currentP = document.createElement('p'); currentP.appendChild(document.createTextNode('')); bubble.insertBefore(currentP, imleç); }
Bundan sonra yapabileceğimiz şey, boş bir metin düğümüne sahip bir paragraf oluşturmak ve onu imlecin önüne eklemektir. Bu bize doğrudan yazabileceğimiz canlı bir düğüm verir.
Daha sonra gelen her karakter için:
function apendChar(karakter, kabarcık, imleç) { if (karakter === '
') { currentP = document.createElement('p'); currentP.appendChild(document.createTextNode('')); bubble.insertBefore(currentP, imleç); } else { currentP.firstChild.textContent += char; } }
Normal bir karakter için metin düğümünü bir karakter genişletiriz. Tarayıcının bunun için düzeni yeniden hesaplamasına gerek yoktur; metin büyüdü ama hiçbir şey hareket etmedi. Bir Newlin için e, yeni bir paragraf oluşturup currentP'yi ileriye taşıyoruz. Düzen, yeni paragraf için bir kez yeniden hesaplama yapar, hepsi bu.
Bu en çok ilk örnekte, sohbet kullanıcı arayüzünde görünür. Kaydırma ve düzen düzeltilmiş olsa bile, gelen her karakter için hala DOM'a yazıyoruz.
Akış hızlı hareket ettiğinde, aslında önemli olmayan güncellemelerle DOM'u zorlarsınız. Çözüm oldukça basit: Gelen metni hemen yazmak yerine bir arabellekte tutun. Yeterince topladıktan sonra hepsini tek seferde DOM'a yazın; floş budur.
Bunu başarmak için basit bir arabellek tutuyoruz ve aynı anda yalnızca tek bir güncelleme planladığımızdan emin oluyoruz. Ateşlendiğinde requestAnimationFrame, biriken her şeyi alır ve tek seferde DOM'a yazar.
beklemede olsun = ''; rafQueued = false olsun;
Yeni bir karakter akışı gerçekleştiğinde, onu ara belleğe ekleriz. Henüz bir yıkama planlanmamışsa, bir tanesini sıraya koyarız:
function onChar(char) { beklemede += char; if (!rafQueued) { rafQueued = true; requestAnimationFrame(flush); } }
RafQueued bayrağı önemlidir. O olmasaydı, her karakter kendi çerçevesini planlardı ve sonuçta düzinelerce gereksiz yıkamayla karşılaşırdınız.
Yıkama tetiklendiğinde, tüm arabelleği tek geçişte boşaltır:
function floş() { for (beklemedeki const char) {appendChar(char); } beklemede = ''; rafQueued = yanlış; autoScroll(); }
Son kareden sonra gelen tüm karakterler, tarayıcı onları boyamadan hemen önce bir araya getirilir. Daha sonra arabelleği temizliyoruz, bayrağı sıfırlıyoruz ve otomatik kaydırmayı bir kez çalıştırıyoruz.
let userScrolled = false; chatEl.addEventListener('scroll', () => { const boşluk = chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight; userScrolled = boşluk > 60; }); function autoScroll() { if (!userScrolled) { chatEl.scrollTop = chatEl.scrollHeight; } }
Boşluk küçükse otomatik kaydırmayı sürdürürüz. Büyürse kullanıcının yukarı kaydırdığını varsayarız ve dururuz. Bu küçük eşik, yeni çizgilerin yüksekliği hafifçe değiştiğinde titremenin önlenmesine yardımcı olur. Ayrıca yeni bir yayın başladığında userScrolled'ı sıfırlamayı unutmayın.
Kaydırma kontrol altına alınınca başka bir sorun ortaya çıkıyor. Mesaj büyüdükçe değişmeye devam ediyor:
Tek satır olarak başlar,
Her şeyi altına iter.
Teknik olarak hiçbir şey bozuk değil, ancak stabil hissetmiyor. Yaygın bir yaklaşım, her güncellemede mesajın tamamını yeniden oluşturmaktır:
bubble.innerHTML = ''; fullText.split('
').forEach(line => { const p = document.createElement('p'); p.textContent = line || '\u00A0'; bubble.appendChild(p); }); bubble.appendChild(imleçEl);
Bu işe yarıyor ama çok fazla iş yapıyor. Her güncelleme DOM'u yok eder ve yeniden oluşturur, her seferinde düzenin yeniden hesaplanmasını zorlar. Bu yüzden her şey değişmeye devam ediyor. Buradaki fikir, mevcut paragrafın içine yazmak ve yalnızca satır sonuna geldiğimizde yeni bir tane oluşturmaktır.
geçerliP = null olsun; function initBubble(kabarcık, imleç) { currentP = document.createElement('p'); currentP.appendChild(document.createTextNode('')); bubble.insertBefore(currentP, imleç); }
Ve sonra onu karakter karakter güncelleyin:
function apendChar(karakter, kabarcık, imleç) { if (karakter === '
') { currentP = document.createElement('p'); currentP.appendChild(document.createTextNode('')); bubble.insertBefore(currentP, imleç); } else { currentP.firstChild.textContent += char; } }
Artık her şeyi yeniden inşa etmiyoruz. Güncellemelerin çoğu yalnızca bir metin düğümünü genişletir; bu da ucuzdur ve büyük düzen değişikliklerini tetiklemez. Ayrıca, artık kaldırıp yeniden eklemeyeceğimiz için, daha önce fark etmiş olabileceğiniz küçük imleç titremesini de düzeltir.
Bu noktada, kullanıcı arayüzü zaten daha iyi hissettiriyor, ancak hala incelikli bir şeyler oluyor. Hala her karakterin DOM'unu güncelliyoruz. Daha yüksek hızlarda bu, çoğunu aslında hiç göremediğiniz birçok küçük güncellemeye dönüşür.
Hemen oluşturmak yerine, gelen karakterleri arabelleğe alabilir ve bunları kare başına bir kez uygulayabiliriz.
beklemede olsun = ''; rafQueued = false olsun; function onChar(char) { beklemede += char; if (!rafQueued) { rafQueued = true; requestAnimationFrame(flush); } }
Bu noktada henüz DOM'a dokunmuyoruz, yalnızca karakterleri geldikçe topluyoruz. Ardından, bir sonraki çerçeve boyanmadan hemen önce her şeyi bir kerede temizliyoruz:
function floş() { for (beklemedeki const char) {appendChar(char); } beklemede = ''; rafQueued = yanlış; autoScroll(); }
Bunlar daha önce birbirine bağlı olan iki şeyi ayırır:
Verilerin ne kadar hızlı ulaştığı ve ne zaman Kullanıcı arayüzü güncellemeleri.
Sonuç aynı görünüyor, ancak tarayıcı daha az çalışıyor, bu da özellikle akış daha yüksek bir hıza ayarlandığında kullanıcı arayüzünün daha akıcı hissetmesine neden oluyor.
Bu değişikliklerin hiçbiri tek başına büyük bir çaba değildir. Ancak bunlar bir kez yerleştirildikten sonra arayüz her güncellemeye körü körüne tepki vermeyi bırakır. İçerik sürekli olarak gelse bile okunması kolaylaşıyor, kontrol edilmesi kolaylaşıyor ve dikkat dağıtıcı şeyler çok daha az oluyor.
İstikrarlı, öngörülebilir ve iyi bir kullanıcı deneyimi sağlamak için dikkate alınması gereken daha fazla husus vardır. Örneğin yayın akışın ortasında iptal edilirse ne olur? Azaltılmış hareket, klavyede gezinme ve ekran okuyucu erişilebilirliği gibi konularda kullanıcı tercihlerine saygı duyulmasını sağlamak için ne yapabiliriz? Şimdi bundan sonrakilere geçelim.
Kesintili Akışları İşleme
Çoğu akış arayüzü, akışı durdurmanın veya iptal etmenin bir yolunu içerir. Bunu demolarda gördük. Ancak durmak çoğu zaman kullanıcı arayüzünü garip bir durumda bırakır. İmleç yanıp sönmeye devam edebilir, düğmeler güncellenmeyebilir ve mesaj, bitmediğine dair net bir gösterge olmaksızın akışın ortasında donabilir.
Sorun, durdurmanın genellikle tek bir şey yapacak şekilde bağlanmış olmasıdır: zamanlayıcıyı iptal etmek. Bu yeterli değil. Ayrıca (1) bekleyen arabelleği temizlemeniz, (2) imleci kaldırmanız, (3) yanıtı eksik olarak işaretlemeniz ve (4) düğmeleri sıfırlamanız gerekir. İşte bunları nasıl başardığımız.
1. Akışı Temiz Bir Şekilde Durdurun
stopStream'in sırasıyla yapması gerekenler:
Zamanlayıcıyı iptal edin ve isStreaming bayrağını çevirin, böylece artık tıklamalar çalışmaz. requestAnimationFrame (RAF) arabelleğini temizleyin, böylece sıradaki hiçbir şey bir sonraki kareye yazılmaz.
function stopStream() { clearTimeout(streamTimer); isStreaming = yanlış; beklemede = ''; rafQueued = yanlış; }
Bekleyen özelliğin temizlenmesi önemlidir, çünkü son akış örneğinden arabelleğe alınan ve henüz temizlenmemiş karakterler olabilir. Temizlemezseniz sonraki requestAnimationFrame etkinleşir, arabelleği boşaltır ve akış resmi olarak durdurulduktan sonra bu karakterleri DOM'a yazar.
Şimdi baloncuğun üzerinde markStopped'ı çağırarak imleci kaldırmaya geçiyoruz:
if (imleçEl && imleçEl.parentNode) imleçEl.remove(); markStopped(aiBubble); stopBtn.style.display = 'yok'; retryBtn.style.display = ''; playBtn.style.display = ''; setStatus('Durduruldu', 'durduruldu'); chat.removeEventListener('kaydırma', onScroll); }
CursorEl.parentNode kontrolü oradadır çünkü stopStream ayrıca akışın ortasında yeni bir mesaj tetiklendiğinde dahili olarak çağrılır ve bu noktada imleç zaten gitmiş olabilir. Ayrılmış bir düğümde kaldır() öğesini çağırmak fırlatır, bu yüzden önce kontrol ederiz.
markStopped baloncuğun altına küçük bir etiket ekler, böylece kullanıcı yanıtın tamamlanmadığını bilir:
function markStopped(bubble) { if (!bubble) return; bubble.classList.add('durdu'); const etiketi = document.createElement('span'); label.className = 'durdurulan etiket'; label.textContent = 'yanıt durduruldu'; bubble.appendChild(etiket); }
Balonun boş kontrolü, AI mesaj öğesi başlatılmadan önce durdurmanın tetiklendiği uç durumu ele alır; bu durum, kullanıcının balon görünmeden önceki 300 ms'lik gecikme sırasında durdur'u tıklatması durumunda gerçekleşebilir.
Yeniden Deneme Seçeneği Sunun
Akışın durması halinde (belki de bir ağ sorunu veya başka bir beklenmeyen hata nedeniyle) kullanıcıya akışı yeniden denemesi için bir yol sağlamamız gerekir. Bunun temel anlamı, kullanıcı arayüzünün en üste kaydırmak, istemi yeniden okumak ve yeniden yazmak için gereken pahalı işi yapmasını engellemektir. Yeniden deneme seçeneğiyle kullanıcının yalnızca bir düğmeye tıklaması gerekir ve akış geçerli konumdan yeniden başlatılır.
Bunun işe yaraması için yayın başladığında şu soruyu cevaplamamız gerekiyor:
let son Soru = ''; function startStream(soru, cevap) { sonSoru = soru; // kurulumun geri kalanı... }
Ardından, yeniden deneme girişimi çalıştırıldığında her şeyi sıfırlarız ve yeniden başlarız:
function retryStream() { if (currentMsgEl && currentMsgEl.parentNode) { currentMsgEl.remove(); } charIndex = 0; userScrolled = yanlış; beklemede = ''; rafQueued = yanlış; isStreaming = doğru; retryBtn.style.display = 'yok'; stopBtn.style.display = ''; setStatus('Akış...', 'akış'); chat.addEventListener('scroll', onScroll, { pasif: true }); setTimeout(() => { initAIMsg(); Tick(lastAnswer); }, 200); }
Sıfırlama kritiktir. Her devletin, tıpkı yeni bir akım gibi, başlangıçtaki değerine dönmesi gerekiyor.
Not: Yalnızca baloncuğu değil, mesaj satırının tamamını (currentMsgEl) kaldırıyoruz. Yalnızca kabarcık kaldırılırsa döşeme t sarmalayıcı ve avatar kalıcı kalır ve yapıyı bozar.
Yayın Ortasında Yeni Bir Mesaj Gönder
Kaçırılması kolay bir son durum daha var. Kullanıcı, akış devam ederken yeni bir mesaj gönderirse, aynı anda DOM'a yazan iki döngüyle karşılaşırsınız. Sonuç karmaşıktır ve farklı yanıtlardan gelen karakterler birbirine karışır.
İşte yapmanız gerekenler: Yeni bir akışa başlamadan önce mevcut akışı durdurun.
function startStream(soru, cevap) { if (isStreaming) { clearTimeout(streamTimer); isStreaming = yanlış; beklemede = ''; rafQueued = yanlış; if (imleçEl && imleçEl.parentNode) imleçEl.remove(); chat.removeEventListener('kaydırma', onScroll); } // şimdi sıfırlayın ve yeni bir başlangıç yapın charIndex = 0; userScrolled = yanlış; isStreaming = doğru; son Soru = soru; // ... }
Burada stopStream'i doğrudan çağırmak yerine temizlemeyi satır içi yapıyoruz çünkü stopStream aynı zamanda markStopped'ı da çağırıyor ve düğmeleri sıfırlıyor. Bir sonraki demoda üç davranışın tümü birbirine bağlanmıştır. Bir yayını başlatabilir, yayının ortasında "Durdur" tuşuna bastığınızda imleç kaybolur, "yanıt durduruldu" etiketi görünür ve "Yeniden Dene" düğmeleri görüntülenir.
Akış arayüzleri genellikle bir fare ile oluşturulup test edilir; bu nedenle, bir tarayıcıda gayet iyi hissedilebilirler, ancak ekran okuyucunun yeni içeriği duyurması gibi, dikkate alınmamış olabilecek diğer durumlarda bozulabilirler. Veya klavyeyle gezinmek, işler güncellendikçe takılıp kalabilir veya odağı kaybedebilir. Ve tabii ki, hareket hassasiyeti olanlar için hareketli metin rahatsız edici, hatta engelleyici olabilir.
İşin iyi yanı, bunlara uyum sağlamak için her şeyi yeniden inşa etmenize gerek olmamasıdır; zaten var olanın üstüne oturan çözümlerle düzeltilebilirler.
Canlı Bölgelerle Yardımcı Teknolojinin Uyumu
Ekran okuyucular, kendi kendine görünen içeriği otomatik olarak duyurmaz. Genellikle kullanıcı onlara taşındığında bir şeyler okurlar. Dolayısıyla, metnin zamanla oluştuğu akışlı bir kullanıcı arayüzünde hiçbir şey duyurulmaz. İçerik oradadır ancak kullanıcı hiçbir şey duymaz.
Çözüm aria-live'dir. Tarayıcıya, kullanıcının odağı değiştirmesine gerek kalmadan bir kapsayıcıyı izlemesini ve güncellemeleri gerçekleştikçe duyurmasını söyler.
role = "log", yardımcı teknolojiye bunun çalışan bir transkript gibi bir güncelleme akışı olduğunu söyler. Bazı araçlar bunu otomatik olarak halleder ancak davranışların tutarlı kalması için açık olmak daha güvenlidir.
Yardımcı teknolojiye bunun çalışan bir transkript gibi bir güncelleme akışı olduğunu söylüyor. Bazı araçlar bunu otomatik olarak halleder ancak davranışların tutarlı kalması için açık olmak daha güvenlidir. aria-atomic='false' yalnızca yeni içeriğin duyurulmasını sağlar. Bu olmadan, bazı ekran okuyucular her güncellemede mesajın tamamını yeniden okumaya çalışır ve bu da hızla kullanılamaz hale gelir.
yalnızca yeni içeriğin duyurulmasını sağlar. Bu olmadan, bazı ekran okuyucular her güncellemede mesajın tamamını yeniden okumaya çalışır ve bu da hızla kullanılamaz hale gelir. aria-live='polite' güncellemeleri kesintiye uğratmak yerine sıraya koyar. İddialı ifadeyi yalnızca hatalar gibi gerçekten acil müdahale gerektiren şeyler için kullanın.
Eksik Durumları İşleme
Daha önce, yayın akışın ortasında durduğunda mesaja bir "Yanıt Durduruldu" etiketi eklemiştik. Görsel olarak bu yeterli. Ancak bir ekran okuyucu için bu değişikliğin duyurulması gerekiyor.
Mesaj, aria-live='polite' ile canlı bir bölgenin içinde olduğundan, DOM'a eklendiğinde etiket otomatik olarak yeni içerik olarak duyurulacaktır. Canlı bölge duyuruyu zaten gerçekleştiriyor, dolayısıyla etiketin kendisinde ek ARIA'ya gerek yok.
Daha sonra görünen Yeniden Dene düğmesinin de bağlama ihtiyacı vardır. Bir ekran okuyucu yalnızca "Yeniden dene, düğme" diyorsa bunun hangi eyleme atıfta bulunduğu açık değildir. Orijinal soruyu içeren bir aria etiketi ekleyerek bunu düzeltebilirsiniz:
retryBtn.setAttribute( 'aria-label', `Yeniden dene: ${lastQuestion.slice(0, 60)}` );
Burada yapabileceğiniz şey, bu etiketi sayfa yüklenirken değil, düğme göründüğünde ayarlamaktır:
retryBtn.style.display = 'satır içi blok'; retryBtn.setAttribute( 'aria-label', `Yeniden dene: ${lastQuestion.slice(0, 60)}` );
Ayrıca durduktan sonra retryBtn.focus()'u da çağırırız. Bu şekilde, klavye kullanıcılarının bir sonraki eylemi bulmak için klavyede Sekme hareketi yapmasına gerek kalmaz.
Yardımcı teknolojiyle test etme: Ekran okuyucuların bunu nasıl duyurduğuna ilişkin varsayımlara güvenmeyin. NVDA (Windows), JAWS (Windows) veya VoiceOver (Mac/iOS) gibi gerçek araçlarla test edin. Tarayıcı DevTools, erişimde nelerin açığa çıktığını size gösterebilir lite ağacı, ancak içeriğin nasıl ses çıkardığını size söyleyemezler. Gerçek bir ekran okuyucu, duyurunun doğru zamanda ve doğru şekilde yapılıp yapılmadığını ortaya çıkaracaktır.
Klavye Gezintisi Hesabı
Kullanıcı arayüzü canlıyken kontrollerin klavyeyle çalışması gerekir, bu nedenle Durdur düğmesinin erişilebilir olması gerekir. Fare kullanmayan biri için, devam eden bir akışı iptal etmenin tek yolu Tab + Enter'dur.
Ekranı kullanma: hiçbiri düğmeleri gizlemek için uygun değildir; bunları sekme sırasından kaldırır. Sorun opacity: 0 veya görünürlük: gizli gibi şeyleri kullanmaktır. Bunlar öğeleri görsel olarak gizler, ancak yine de odaklanabilirler, böylece kullanıcılar göremedikleri bir şeye sekmeyle ulaşırlar.
Odaklama halkasının klavyede gezinme için görünmesi ancak fare tıklamaları için görünmemesi için :focus-visible kullanın:
btn:focus-visible { taslak: 2px katı #1d9e75; anahat uzaklığı: 2 piksel; }
Mesajın içindeki imlecin aria-hidden='true' olması gerekir. Bu sadece görsel. Bu olmadan, bazı ekran okuyucular onu metin olarak okumaya çalışır ve bu da dikkat dağıtıcı olur.
Hemen hemen her yapay zeka arayüzünde gördüğümüz daktilo efekti, sürekli hareket üretir. Daha önce de tartıştığımız gibi, belirli miktarda hareket devre dışı bırakabilir. Neyse ki tarayıcılar, kullanıcının hareket tercihlerini işletim sistemi düzeyinde tespit eden tercihler-azaltılmış hareketi açığa çıkarır.
Akış için en iyi yaklaşım basittir: Animasyonu atlayın ve yanıtın tamamını bir kerede oluşturun. İçerik yalnızca hareket olmadan aynı kalır.
const azaltılmışMotion = window.matchMedia( '(azaltılmış hareketi tercih eder: azalt)' ).eşleşmeler;
if (reducedMotion) { initAIMsg(); for (metnin const char'ı) AppendChar(char); if (imleçEl && imleçEl.parentNode) imleçEl.remove(); Tamamlandı(); geri dönmek; } işaretle(metin); // normal animasyon
CSS'de imlecin yanıp sönmesinin de durması gerekir. Küçük bir ayrıntı olmasına rağmen, yanıp sönen bir imleç öğesi, yanıp sönen içerik olarak sayılır.
@media (azaltılmış hareketi tercih eder: azalt) { .imleç { animasyon: yok; opaklık: 1; } }
İşte başlıyoruz! Aşağıdaki demo bu makaledeki her şeyi bir araya getiriyor, böylece bu kalıpların pratikte nasıl çalıştığını görebilirsiniz. Aynı zamanda azaltılmış bir hareket geçişi içerir, böylece anında oluşturma sürümünü kolayca test edebilirsiniz.
Akışın kendisi çoğunlukla çözüldü. Sunucudan istemciye veri almak artık zor kısım değil. Kırılan şey, bunun üzerindeki kullanıcı arayüzüdür.
İçerik sürekli olarak güncellendiğinde kaydırma davranışı, düzen kararlılığı, oluşturma zamanlaması ve arayüzün kullanıcı eylemlerine nasıl tepki verdiği gibi küçük şeyler önem kazanmaya başlar. Bunlar iyi bir şekilde ele alınmazsa kullanıcı arayüzü dengesiz ve kullanımı zor hissedilir.
Bu makaledeki kalıplar bu sorunu şu şekilde düzeltir:
Kaydırma pozisyonunun kullanıcının kontrolünde tutulması,
Yalnızca değişenleri güncelleme,
Kare başına işlemelerin toplu işlenmesi,
Durdurma ve yeniden deneme eylemlerinin işlenmesi ve
Arayüzü erişilebilir hale getirmek.
Bunların hepsine her zaman ihtiyacınız yok. Ancak akış söz konusu olduğunda işlerin genellikle ters gittiği yerler bunlardır.
Sunucu Tarafından Gönderilen Olayları Kullanma
Bir bağlantının nasıl açılacağı, olayların nasıl yönetileceği ve gerektiğinde yeniden nasıl bağlanılacağı. Bu taşıma katmanıdır, burada her şey üzerine kuruludur.
Bir bağlantının nasıl açılacağı, olayların nasıl yönetileceği ve gerektiğinde yeniden nasıl bağlanılacağı. Bu taşıma katmanıdır, burada her şey üzerine kuruludur. Akış API'sı
Fetch'ten doğrudan veri akışı. SSE'den daha fazla kontrole ihtiyacınız olduğunda kullanışlıdır.
Verileri doğrudan . SSE'den daha fazla kontrole ihtiyacınız olduğunda kullanışlıdır. Chrome DevTools Performans paneli
Performans iyileştirmelerini doğrulayabilmeniz için düzen yeniden hesaplamalarını ve boya maliyetlerini görmenize yardımcı olur.
Performans iyileştirmelerini doğrulayabilmeniz için düzen yeniden hesaplamalarını ve boya maliyetlerini görmenize yardımcı olur. “DOM Boyutlarının Büyüklüğü Etkileşimi Nasıl Etkiler ve Bu Konuda Ne Yapabilirsiniz?”, Jeremy Wagner
Büyük DOM ağaçları neden işleri yavaşlatır ve uzun yayın oturumlarında bunların nasıl kontrol altında tutulacağı.