Ders 19 / 22
Erişilebilir Hata Sunumu
Hata iletisinin alanla programlı olarak ilişkilendirilmesi, geçersizlik bildiriminin verilmesi, belge sırasına göre kurulan hata özeti, odağın taşınması ve iletinin hangi anda yazılacağı.
İçindekiler
Önceki ders her alan için bir hata kodu ve parametresi üretti. Sıradaki soru bunların ekranda nasıl görüneceği değil: bir hata iletisi, yalnız görüldüğünde iş görüyorsa yarım kalmıştır. Ekran okuyucuyla gezen bir gözlemci ölçüm giriş formunu gönderdiğinde, kırmızı çerçeveyi görmez; alanın altına yazılan metnin o alanla birlikte okunması kendiliğinden olmaz.
Bu ders hatanın algılanabilir hâle getirilmesini ele alır: ileti hangi ögeye bağlanır, alanın geçersiz olduğu nasıl bildirilir, gönderim reddedildiğinde odak nereye gider ve ileti hangi anda yazılır.
Görmeyen Kullanıcı İçin Hata Nedir
Görsel sunum üç ipucu kullanır: renk, konum ve simge. Üçü de tek başına yetersizdir.
Renk tek gösterge olamaz. WCAG 1.4.1 ölçütü bunu doğrudan söyler: bilgi yalnız renkle aktarılamaz. Kırmızı çerçeve renk ayrımı olmayan bir kullanıcı için sıradan bir çerçevedir. Rengin yanında metin gerekir.
Konum programlı bir bağ değildir. İletinin alanın hemen altında durması gözle kurulan bir ilişkidir. Erişilebilirlik ağacında iki öge arasında bir bağ yoksa, alana odaklanan kullanıcı iletiyi hiç duymaz. Bağ açıkça kurulmalıdır.
Hata bildirimi ile hatanın tanımı ayrı ölçütlerdir. WCAG 3.3.1 hatanın saptandığının metinle bildirilmesini, 3.3.3 ise mümkünse düzeltme önerisinin sunulmasını ister. “Geçersiz değer” birincisini karşılar, ikincisini karşılamaz; “−60 ile 60 arasında bir değer girin” ikisini birden karşılar. Bu yüzden önceki dersteki hata parametreleri iletinin içine yazılır.
İletiyi Alana Bağlamak
Bağ, alanın ek açıklama listesiyle kurulur. Web’in Temelleri ve HTML kursunda tanımlanan bu düzenek, alana odaklanıldığında etiketten sonra okunacak metinleri belirler. Alanın hem bir ipucu hem bir hatası varsa ikisi de listeye girer ve listedeki sıra okunma sırasıdır.
// hata-isaretlemesi.mjs — alan, ipucu, hata ve ozetin iliskilendirilmesi const ILETILER = { zorunlu: () => "Bu alan zorunludur.", "sayı_değil": () => "Sayı girin; ondalık ayırıcı olarak virgül kullanabilirsiniz.", "aralık_dışı": (p) => `${p.enAz} ile ${p.enCok} arasında bir değer girin.`, gelecekte: () => "Ölçüm tarihi gelecekte olamaz.", }; // Alanlar belgedeki siralarina gore tanimlanir; sira odak kararini belirler. const ALANLAR = [ { ad: "istasyon", etiket: "İstasyon", ipucu: null }, { ad: "tarih", etiket: "Ölçüm tarihi", ipucu: "Gün-ay-yıl biçiminde." }, { ad: "deger", etiket: "Ölçüm değeri", ipucu: "Santigrat derece." }, { ad: "not", etiket: "Not", ipucu: null }, ]; function alanIsaretlemesi(alan, hata) { const kimlik = `alan-${alan.ad}`; const ipucuKimligi = alan.ipucu ? `${kimlik}-ipucu` : null; const hataKimligi = hata ? `${kimlik}-hata` : null; // Sira onemlidir: once ipucu, sonra hata okunur. const acikla = [ipucuKimligi, hataKimligi].filter(Boolean).join(" "); const satirlar = [`<label for="${kimlik}">${alan.etiket}</label>`]; satirlar.push( `<input id="${kimlik}" name="${alan.ad}"` + (acikla ? ` aria-describedby="${acikla}"` : "") + (hata ? ` aria-invalid="true"` : "") + ">"); if (alan.ipucu) satirlar.push(`<p id="${ipucuKimligi}">${alan.ipucu}</p>`); if (hata) satirlar.push(`<p id="${hataKimligi}">${ILETILER[hata.kod](hata.param)}</p>`); return satirlar.join("\n"); } function ozet(hatalar) { // Ozet, hatalarin saptanma sirasina gore degil belgedeki alan sirasina gore dizilir. const sirali = ALANLAR .map((a) => [a, hatalar.find((h) => h.yol === a.ad)]) .filter(([, h]) => h); const maddeler = sirali.map(([a, h]) => ` <li><a href="#alan-${a.ad}">${a.etiket}: ${ILETILER[h.kod](h.param)}</a></li>`); return [ `<div role="alert" tabindex="-1" id="hata-ozeti">`, ` <h2>${sirali.length} alan düzeltilmeli</h2>`, " <ul>", ...maddeler, " </ul>", "</div>", ].join("\n"); } // Semadan gelen hata listesi (saptanma sirasi belge sirasindan farkli). const HATALAR = [ { yol: "deger", kod: "aralık_dışı", param: { enAz: -60, enCok: 60 } }, { yol: "tarih", kod: "gelecekte" }, ]; console.log(ozet(HATALAR)); console.log(""); for (const alan of ALANLAR) { console.log(alanIsaretlemesi(alan, HATALAR.find((h) => h.yol === alan.ad))); } // --- Odak hedefi ----------------------------------------------------------- const ilkGecersiz = ALANLAR.find((a) => HATALAR.some((h) => h.yol === a.ad)); console.log("\nhata sayısı :", HATALAR.length); console.log("odak hedefi :", HATALAR.length > 1 ? "#hata-ozeti" : `#alan-${ilkGecersiz.ad}`); console.log("tek hata olsaydı:", `#alan-${ilkGecersiz.ad}`);
<div role="alert" tabindex="-1" id="hata-ozeti"> <h2>2 alan düzeltilmeli</h2> <ul> <li><a href="#alan-tarih">Ölçüm tarihi: Ölçüm tarihi gelecekte olamaz.</a></li> <li><a href="#alan-deger">Ölçüm değeri: -60 ile 60 arasında bir değer girin.</a></li> </ul> </div> <label for="alan-istasyon">İstasyon</label> <input id="alan-istasyon" name="istasyon"> <label for="alan-tarih">Ölçüm tarihi</label> <input id="alan-tarih" name="tarih" aria-describedby="alan-tarih-ipucu alan-tarih-hata" aria-invalid="true"> <p id="alan-tarih-ipucu">Gün-ay-yıl biçiminde.</p> <p id="alan-tarih-hata">Ölçüm tarihi gelecekte olamaz.</p> <label for="alan-deger">Ölçüm değeri</label> <input id="alan-deger" name="deger" aria-describedby="alan-deger-ipucu alan-deger-hata" aria-invalid="true"> <p id="alan-deger-ipucu">Santigrat derece.</p> <p id="alan-deger-hata">-60 ile 60 arasında bir değer girin.</p> <label for="alan-not">Not</label> <input id="alan-not" name="not"> hata sayısı : 2 odak hedefi : #hata-ozeti tek hata olsaydı: #alan-tarih
Üretilen işaretlemede dört karar görünür.
Ek açıklama listesi hem ipucunu hem hatayı taşır. Hata çıktığında ipucunun listeden düşürülmesi sık yapılan bir yanlıştır; kullanıcı hatayı duyar ama beklenen biçimi öğrenemez. İkisi birlikte kalır ve ipucu önce okunur.
Geçersizlik ayrıca bildirilir. İletinin okunması, alanın geçersiz olduğunun
bildirilmesiyle aynı şey değildir. aria-invalid="true" alanın durumunu erişilebilirlik
ağacına yazar; ileti metniyle birlikte kullanılır, onun yerine geçmez. Aynı işi yapan bir
başka öznitelik daha vardır — hata iletisini doğrudan gösteren aria-errormessage — ancak
o yalnız geçersizlik bildirimi verildiğinde anlam taşır; ek açıklama bağı ise her durumda
işler. Sağlam yol, ek açıklama bağını temel almaktır.
Hatası olmayan alanda hiçbir öznitelik bulunmaz. İlk ve son alanda aria-invalid
yazılmamış; alan geçerliyken aria-invalid="false" yazmak da doğrudur ama gereksizdir.
Önemli olan, hata düzeldiğinde özniteliğin kaldırılmasıdır; kalırsa kullanıcı düzelmiş
bir alanı geçersiz duyar.
Özet belge sırasına göre dizilir. Şemadan gelen hata listesi deger ile başlıyordu,
özette tarih önce geldi. Kullanıcı özetteki maddelere yukarıdan aşağıya baktığında
formda da yukarıdan aşağıya ilerlemelidir; saptanma sırası bir uygulama ayrıntısıdır ve
ekrana sızmamalıdır.
Özet Kutusu ve Odağın Taşınması
Gönderim reddedildiğinde kullanıcı iki şeyi bilmelidir: gönderimin gerçekleşmediğini ve neyin düzeltileceğini. Sayfanın hiçbir yeri değişmemiş gibi görünüyorsa — özellikle uzun bir formda, hatalar ekranın dışında kaldıysa — kullanıcı düğmenin çalışmadığını sanır.
Hata özeti formun başında durur, hata sayısını söyler ve her maddesi ilgili alana bağlantı verir. Bağlantı olması önemlidir: klavyeyle gezen kullanıcı maddeyi seçip doğrudan alana gider. Madde metni, alanın etiketini ve iletiyi birlikte taşır; yalnız ileti yazıldığında hangi alandan söz edildiği anlaşılmaz.
Odak özet kutusuna taşınır. Kutunun odaklanabilir olması için sekme sırasına
girmeyen bir odak hedefi olarak işaretlenmesi gerekir — çıktıdaki tabindex="-1" bunu
yapar; öge programla odaklanabilir ama sekme turuna katılmaz.
Hata sayısına göre iki davranış ayrılır. Tek hata varsa odağı doğrudan o alana taşımak kullanıcıyı bir adım öne alır. Birden çok hata varsa özet kutusuna taşımak, kullanıcıya önce durumun tamamını verir. Çıktının son üç satırı bu kararı hesaplıyor.
Odak taşımanın bir sınırı vardır: kullanıcının odağını beklemediği bir yere almak da bir kesintidir. Kural, odağın yalnız kullanıcının kendi eylemine yanıt olarak taşınmasıdır. Gönderim düğmesine basmak böyle bir eylemdir; arka planda tamamlanan bir istek değildir.
Bildirimin Anı
İşaretleme doğru olsa bile iletinin ne zaman göründüğü ayrı bir karardır. Kullanıcı ölçüm değerini yazarken her tuşta “geçersiz” demek, henüz bitmemiş bir girdiyi hatalı ilan eder ve yazmayı zorlaştırır.
// gosterim-ani.mjs — hata iletisinin ne zaman yazilacagi function gosterilirMi({ hataVar, dokunuldu, gonderimDenendi, hataGosterilmisti }) { if (!hataVar) return false; // hata yoksa gosterilecek sey yok if (gonderimDenendi) return true; // gonderimden sonra hepsi gorunur if (dokunuldu) return true; // alan terk edildi return hataGosterilmisti; // bir kez gosterildiyse izlemeye devam } // Hata yokken cevap her zaman "hayir"; tablo yalniz hatali alani sayiyor. const DURUMLAR = []; for (const dokunuldu of [false, true]) for (const gonderimDenendi of [false, true]) for (const hataGosterilmisti of [false, true]) DURUMLAR.push({ hataVar: true, dokunuldu, gonderimDenendi, hataGosterilmisti }); const im = (b) => (b ? "evet" : "hayır"); console.log("dokunuldu gönderim önceden → gösterilir"); for (const d of DURUMLAR) { console.log( im(d.dokunuldu).padEnd(11) + im(d.gonderimDenendi).padEnd(10) + im(d.hataGosterilmisti).padEnd(9) + " " + im(gosterilirMi(d))); } // Kullanicinin gercek yolculugu: yazarken hata yok, terk edince var, duzeltince kayboluyor. console.log("\nadım adım bir alanın yaşamı"); const YOL = [ ["boş alana '-' yazıldı", { hataVar: true, dokunuldu: false, gonderimDenendi: false }], ["'-4' yazıldı", { hataVar: false, dokunuldu: false, gonderimDenendi: false }], ["alan silindi", { hataVar: true, dokunuldu: false, gonderimDenendi: false }], ["alan terk edildi", { hataVar: true, dokunuldu: true, gonderimDenendi: false }], ["geri dönülüp '9' yazıldı",{ hataVar: false, dokunuldu: true, gonderimDenendi: false }], ["'900' yapıldı", { hataVar: true, dokunuldu: true, gonderimDenendi: false }], ]; let onceden = false; for (const [ad, kismi] of YOL) { const g = gosterilirMi({ ...kismi, hataGosterilmisti: onceden }); onceden = g; console.log(` ${ad.padEnd(27)} hata=${im(kismi.hataVar).padEnd(6)} → ${im(g)}`); }
dokunuldu gönderim önceden → gösterilir hayır hayır hayır hayır hayır hayır evet evet hayır evet hayır evet hayır evet evet evet evet hayır hayır evet evet hayır evet evet evet evet hayır evet evet evet evet evet adım adım bir alanın yaşamı boş alana '-' yazıldı hata=evet → hayır '-4' yazıldı hata=hayır → hayır alan silindi hata=evet → hayır alan terk edildi hata=evet → evet geri dönülüp '9' yazıldı hata=hayır → hayır '900' yapıldı hata=evet → evet
Tablonun ikinci satırı kuralın çekirdeğidir: alan henüz terk edilmemiş ve gönderim denenmemiş olsa bile, hata bir kez gösterildiyse gösterilmeye devam eder. Bunun sonucu, ikinci çıktı bloğunun son iki satırında görünüyor — alan bir kez hatalı bulunduktan sonra kullanıcı yazdıkça ileti anında güncelleniyor; düzeltme yapılınca kayboluyor, yeniden bozulunca dönüyor.
İlk üç satırdaki davranış da bilinçlidir. Kullanıcı boş alana eksi işareti yazdığında değer henüz geçersizdir ama ileti çıkmaz; girdi devam ediyordur. İleti ilk kez alan terk edildiğinde belirir.
Bu kural, önceki dersteki dokunulan kümesi ile gonderimSayisi sayacının neden ayrı
tutulduğunu açıklıyor. İkisi de “bu alanı gösterebilir miyim” sorusunun girdisidir ve
farklı anları işaretler.
Canlı Bölge mi, Odak mı
Sayfada beliren bir metnin ekran okuyucu tarafından okunmasının iki yolu vardır: odağı oraya taşımak ya da metni bir canlı bölge içine koymak. WCAG 4.1.3 ölçütü, odak almayan durum iletilerinin de duyurulmasını ister.
Ayrım şu soruyla yapılır: kullanıcının o an bir şey yapması gerekiyor mu?
Odak taşıma kullanıcıyı yönlendirir ve akışını keser. Gönderim reddedildiğinde
uygundur; kullanıcı zaten bir sonuç bekliyordur ve devam etmek için bir şey yapması
gerekir. Çıktıdaki özet kutusu role="alert" taşıyor; bu rol, kutunun içeriğini
belirdiği anda duyurulacak biçimde işaretler ve odak taşımayla birlikte kullanılır.
Canlı bölge, kullanıcının akışını kesmeden bilgi verir. Alan alan doğrulanan bir formda her ileti için odak taşınmaz; iletiler kibar öncelikli bir bölgede birikir ve kullanıcı yazmayı bıraktığında okunur.
Aşırı bildirim gerçek bir kusurdur. Her tuşta güncellenen bir canlı bölge, ekran okuyucuyu sürekli konuşturur ve kullanıcının kendi yazdığını duymasını engeller. Bu yüzden alan alan doğrulama sonuçları gecikmeli tetiklemeyle seyreltilir, gönderim sonucu ise tek seferlik ve kesin bir duyuru olarak verilir.
Özet
- Renk tek gösterge olamaz ve konum programlı bir bağ değildir; ileti alana ek açıklama bağıyla açıkça iliştirilir.
- Ek açıklama listesi hem ipucunu hem hatayı sırayla taşır; geçersizlik bildirimi iletinin yerine geçmez, onunla birlikte verilir ve hata düzeldiğinde kaldırılır.
- Hata özeti belge sırasına göre dizilir, hata sayısını söyler ve her maddesi ilgili alana bağlantı verir.
- Odak, tek hata varsa doğrudan alana, birden çok hata varsa özet kutusuna taşınır; taşıma yalnız kullanıcının kendi eylemine yanıt olarak yapılır.
- İleti ilk kez alan terk edildiğinde ya da gönderim denendiğinde yazılır; bir kez yazıldıktan sonra her değişiklikte güncellenir.
- Odağı kesmeyen bildirimler canlı bölgeyle verilir; her tuşta güncellenen bir bölge aşırı bildirim üretir ve gecikmeli tetiklemeyle seyreltilir.
Sonraki Adım
Ölçüm giriş formu artık değerini tutuyor, kurallarını iki tarafta aynı biçimde uyguluyor ve hatasını herkesin algılayabileceği biçimde sunuyor. Geriye formun sorduğu bir soru kaldı: bu ölçümü kim giriyor? Kuzey Yamaç kayıtlarına herkes yazamaz; gözlemcinin önce tanınması gerekir. Sonraki ders kimlik akışının istemci tarafını ele alır — sunucunun verdiği belirtecin tarayıcıda nerede saklanacağı ve bu kararın hangi riskleri açıp hangi riskleri kapattığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.