İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat