Ders 11 / 27
Ekran Okuyucu Deneyimi
Belgenin gezinme düzlemleri ve düzlem kusurlarının denetimi, duyurunun neyi taşıdığı, canlı bölgelerin kibar ve atılgan önceliği ile duyuru kuyruğunun davranışı.
İçindekiler
Önceki ders odağın nereye gittiğini hesapladı. Odak oraya vardığında kullanıcının ne öğrendiği ayrı bir sorudur ve yanıtı iki kanaldan gelir: ögenin kendisinden gelen bilgi ve ögeden bağımsız olarak değişen içeriğin bildirimi.
Bu dersin konusu iki kanaldır. Ekran okuyucuların iç işleyişi ve söyledikleri sözcükler araca göre değişir; burada anlatılan, ağaçta tanımlı olan bilginin ne olduğu ve duyuruların hangi öncelik kurallarına göre sıraya girdiğidir. Bu kurallar araca değil tanıma bağlıdır.
Sayfa Doğrusal Okunmaz
Ekran okuyucu kullanıcısı bir sayfayı baştan sona dinlemez; bu, uzun bir belgede kullanılamaz bir yol olurdu. Kullanılan yol, belgenin gezinme düzlemleri arasında atlamaktır: yer imlerinden bölüme, başlıklardan konuya, bağlantı listesinden hedefe, denetim listesinden forma.
Her düzlem belgeden türetilir ve türetme kusurları düzlemi kullanılmaz kılar. Aşağıdaki betik ölçüm listesi sayfasından üç düzlemi çıkarıyor ve her birini kendi ölçütüyle denetliyor.
// duzlem.mjs — bir belgeden uc gezinme duzleminin turetilmesi ve kusur denetimi const YER_IMI = { header: "banner", nav: "navigation", main: "main", aside: "complementary", footer: "contentinfo", section: "region", form: "form" }; // ust duzey: belgenin dogrudan cocugu mu const BELGE = [ { oge: "header", ustDuzey: true }, { oge: "nav", ustDuzey: false, etiket: "Ağ genelinde gezinme" }, { oge: "main", ustDuzey: true }, { oge: "h1", metin: "Kuzey Yamaç Ölçüm İstasyonu" }, { oge: "form", ustDuzey: false, etiket: "Süzgeç" }, { oge: "a", metin: "Ayrıntı", hedef: "/olcum/T-01" }, { oge: "h3", metin: "Günlük Ölçümler" }, { oge: "a", metin: "Ayrıntı", hedef: "/olcum/T-02" }, { oge: "a", metin: "buraya tıklayın", hedef: "/yontem.html" }, { oge: "h2", metin: "Uyarılar" }, { oge: "nav", ustDuzey: false }, { oge: "a", metin: "Ölçüm yöntemi belgesi", hedef: "/yontem.html" }, { oge: "footer", ustDuzey: true }, ]; const BELIRSIZ = new Set(["buraya tıklayın", "tıklayın", "devamı", "daha fazla", "bağlantı"]); function yerImleri(belge) { const liste = []; for (const d of belge) { let rol = YER_IMI[d.oge]; if (!rol) continue; if ((d.oge === "header" || d.oge === "footer") && !d.ustDuzey) continue; if ((d.oge === "section" || d.oge === "form") && !d.etiket) continue; liste.push({ rol, etiket: d.etiket ?? null }); } return liste; } const basliklar = (belge) => belge.filter((d) => /^h[1-6]$/.test(d.oge)).map((d) => ({ duzey: Number(d.oge[1]), metin: d.metin })); const baglantilar = (belge) => belge.filter((d) => d.oge === "a"); console.log("yer imi duzlemi:"); for (const y of yerImleri(BELGE)) console.log(` ${y.rol}${y.etiket ? ': "' + y.etiket + '"' : ""}`); console.log("\nbaslik duzlemi:"); for (const b of basliklar(BELGE)) console.log(` ${" ".repeat(b.duzey - 1)}h${b.duzey} ${b.metin}`); console.log("\nbaglanti duzlemi:"); for (const b of baglantilar(BELGE)) console.log(` "${b.metin}" -> ${b.hedef}`); console.log("\nkusur denetimi:"); const bulgular = []; // 1. Ayni turden adsiz yer imi birden fazla const sayim = {}; for (const y of yerImleri(BELGE)) sayim[y.rol] = (sayim[y.rol] ?? 0) + 1; for (const y of yerImleri(BELGE)) { if (sayim[y.rol] > 1 && !y.etiket) bulgular.push(`adsiz ${y.rol} yer imi, ayni rolden ${sayim[y.rol]} tane var`); } // 2. Baslik duzeyi atlamasi let onceki = 0; for (const b of basliklar(BELGE)) { if (onceki && b.duzey > onceki + 1) bulgular.push(`baslik duzeyi atlandi: h${onceki} -> h${b.duzey} ("${b.metin}")`); onceki = b.duzey; } // 3. Ayni metin farkli hedef / farkli metin ayni hedef / belirsiz metin const metinler = {}; const hedefler = {}; for (const b of baglantilar(BELGE)) { (metinler[b.metin] ??= new Set()).add(b.hedef); (hedefler[b.hedef] ??= new Set()).add(b.metin); if (BELIRSIZ.has(b.metin.toLocaleLowerCase("tr"))) bulgular.push(`baglamdan kopuk baglanti metni: "${b.metin}"`); } for (const [m, h] of Object.entries(metinler)) { if (h.size > 1) bulgular.push(`"${m}" metni ${h.size} farkli hedefe gidiyor`); } for (const [h, m] of Object.entries(hedefler)) { if (m.size > 1) bulgular.push(`${h} hedefine ${m.size} farkli metinle baglaniliyor`); } for (const b of bulgular) console.log(" - " + b); console.log(` toplam ${bulgular.length} bulgu`);
yer imi duzlemi:
banner
navigation: "Ağ genelinde gezinme"
main
form: "Süzgeç"
navigation
contentinfo
baslik duzlemi:
h1 Kuzey Yamaç Ölçüm İstasyonu
h3 Günlük Ölçümler
h2 Uyarılar
baglanti duzlemi:
"Ayrıntı" -> /olcum/T-01
"Ayrıntı" -> /olcum/T-02
"buraya tıklayın" -> /yontem.html
"Ölçüm yöntemi belgesi" -> /yontem.html
kusur denetimi:
- adsiz navigation yer imi, ayni rolden 2 tane var
- baslik duzeyi atlandi: h1 -> h3 ("Günlük Ölçümler")
- baglamdan kopuk baglanti metni: "buraya tıklayın"
- "Ayrıntı" metni 2 farkli hedefe gidiyor
- /yontem.html hedefine 2 farkli metinle baglaniliyor
toplam 5 bulgu
Beş bulgunun her biri ayrı bir düzlemi bozuyor.
Adsız ikinci gezinme bloğu yer imi listesinde ayırt edilemez: kullanıcı iki “navigation” girdisi görür ve hangisinin sayfa içi gezinme olduğunu ancak ikisini de açarak öğrenir. Çözüm ada bakar, yapıya değil.
Başlık düzeyi atlaması ana hattı bozar. Çıktının girintileri sorunu gösteriyor: h1’den sonra gelen h3, ardından gelen h2’nin altına düşmesi gereken bir konuyu onun üstüne koyuyor. Başlık düzlemi belgenin içindekiler dizinidir; atlanan düzey, dizinde var olmayan bir kırılma üretir.
Bağlantı düzleminde üç kusur birlikte görünüyor. Bağlamdan kopuk metin, listede tek başına okunduğunda hiçbir şey söylemez. Aynı metnin iki farklı hedefe gitmesi, listede ayırt edilemeyen iki giriş üretir — ölçüm tablosundaki her satırın “Ayrıntı” bağlantısı tam bu durumdur ve çözümü bağlantı metnine ölçüm kodunu eklemektir. Aynı hedefe iki farklı metinle bağlanmak ise ters yönde bir kusurdur: kullanıcı iki ayrı sayfa olduğunu sanır.
Duyuru Neyi Taşır
Odak bir ögeye vardığında kullanıcıya iletilen bilgi, o ögenin ağaçtaki dört alanından
gelir: rolü, erişilebilir adı, değeri ve durumları; buna bir de
aria-describedby ile bağlanan ek açıklama eklenir. İletim sırası ve sözcükler araca
göre değişir, alanların içeriği değişmez.
Buradan iki pratik kural çıkar. Birincisi, ada rol adı yazılmaz: adı “Süz düğmesi” olan bir düğme, rolüyle birlikte iki kez düğme diye anılır. İkincisi, ek açıklama ada eklenmez, adın ardından gelir; uzun yardım metni ada değil açıklamaya yazılır, yoksa denetimin adı bir paragrafa dönüşür.
Canlı Bölgeler ve Öncelik
Sayfa yenilenmeden değişen içerik ayrı bir sorundur: kullanıcı orada değilse değişimi öğrenemez. Canlı bölge (live region), içeriği değiştiğinde bildirilen bölümdür ve Web Temelleri ve HTML kursunda hata özeti için kurulmuştu.
Bölgenin iki önceliği vardır. Kibar (polite) öncelik, bildirimi sıraya koyar ve
kullanıcının o anki okumasını bölmez; durum iletileri buraya girer. Atılgan (assertive)
öncelik, bekleyenleri geçersiz kılarak öne geçer; yalnızca kullanıcının işini durduran
bildirimler —veri kaybı, gönderim hatası— buraya girer. role="status" bildirimi kibar,
role="alert" bildirimi atılgan bir bölge kurar.
Aşağıdaki betik bu önceliği bir kuyruk olarak modelliyor. Model, iletimin ne kadar süreceğini değil, tanımdaki önceliğin sıraya ne yaptığını gösterir.
// duyuru.mjs — canli bolge duyurularinin kibar/atilgan onceligiyle kuyruklanma modeli const BOLGE = { "suzgec-durumu": { rol: "status", oncelik: "kibar" }, "gonderim-durumu": { rol: "status", oncelik: "kibar" }, "hata-ozeti": { rol: "alert", oncelik: "atilgan" }, }; const SURE = 2; // bir duyurunun kapladigi an sayisi function calistir(baslik, olaylar, sonAn) { console.log(baslik); console.log(" an " + "gelen".padEnd(38) + "duyurulan".padEnd(24) + "kuyruk"); let kuyruk = []; // bekleyen kibar duyurular let etkin = null; // { metin, kalan } let degistirilen = 0, atilan = 0; for (let an = 0; an <= sonAn; an++) { const gelen = olaylar.filter((o) => o.an === an); let gelenMetin = []; for (const o of gelen) { const b = BOLGE[o.bolge]; gelenMetin.push(`${o.bolge}: ${o.metin}`); if (b.oncelik === "atilgan") { atilan += kuyruk.length + (etkin ? 1 : 0); kuyruk = []; etkin = { metin: o.metin, kalan: SURE }; // suren duyuruyu boler } else { const i = kuyruk.findIndex((k) => k.bolge === o.bolge); if (i >= 0) { kuyruk[i] = { ...o }; degistirilen++; } // bolgenin icerigi yenilendi else kuyruk.push({ ...o }); } } if (!etkin && kuyruk.length > 0) etkin = { metin: kuyruk.shift().metin, kalan: SURE }; const duyurulan = etkin && etkin.kalan === SURE ? etkin.metin : etkin ? "(suruyor)" : "—"; console.log( ` ${String(an).padStart(2)} ${(gelenMetin.join(" | ") || "—").padEnd(38)}` + `${duyurulan.padEnd(24)}${kuyruk.length === 0 ? "—" : kuyruk.map((k) => k.metin).join(" | ")}`, ); if (etkin && --etkin.kalan === 0) etkin = null; } console.log(` yenilenen bekleyen duyuru: ${degistirilen}, atilgan duyurunun attigi: ${atilan}`); } const HER_TUSTA = [ { an: 0, bolge: "suzgec-durumu", metin: "12 ölçüm listeleniyor" }, { an: 1, bolge: "suzgec-durumu", metin: "5 ölçüm listeleniyor" }, { an: 2, bolge: "suzgec-durumu", metin: "3 ölçüm listeleniyor" }, { an: 3, bolge: "gonderim-durumu", metin: "Kayıt gönderiliyor" }, { an: 4, bolge: "hata-ozeti", metin: "Kayıt gönderilemedi" }, { an: 7, bolge: "gonderim-durumu", metin: "Kayıt gönderildi" }, ]; const GECIKMELI = [ { an: 2, bolge: "suzgec-durumu", metin: "3 ölçüm listeleniyor" }, { an: 3, bolge: "gonderim-durumu", metin: "Kayıt gönderiliyor" }, { an: 6, bolge: "hata-ozeti", metin: "Kayıt gönderilemedi" }, { an: 9, bolge: "gonderim-durumu", metin: "Kayıt gönderildi" }, ]; calistir("her tus vurusunda duyuru:", HER_TUSTA, 9); console.log(""); calistir("gecikmeli tetiklemeyle tek duyuru:", GECIKMELI, 11);
her tus vurusunda duyuru: an gelen duyurulan kuyruk 0 suzgec-durumu: 12 ölçüm listeleniyor 12 ölçüm listeleniyor — 1 suzgec-durumu: 5 ölçüm listeleniyor (suruyor) 5 ölçüm listeleniyor 2 suzgec-durumu: 3 ölçüm listeleniyor 3 ölçüm listeleniyor — 3 gonderim-durumu: Kayıt gönderiliyor (suruyor) Kayıt gönderiliyor 4 hata-ozeti: Kayıt gönderilemedi Kayıt gönderilemedi — 5 — (suruyor) — 6 — — — 7 gonderim-durumu: Kayıt gönderildi Kayıt gönderildi — 8 — (suruyor) — 9 — — — yenilenen bekleyen duyuru: 1, atilgan duyurunun attigi: 1 gecikmeli tetiklemeyle tek duyuru: an gelen duyurulan kuyruk 0 — — — 1 — — — 2 suzgec-durumu: 3 ölçüm listeleniyor 3 ölçüm listeleniyor — 3 gonderim-durumu: Kayıt gönderiliyor (suruyor) Kayıt gönderiliyor 4 — Kayıt gönderiliyor — 5 — (suruyor) — 6 hata-ozeti: Kayıt gönderilemedi Kayıt gönderilemedi — 7 — (suruyor) — 8 — — — 9 gonderim-durumu: Kayıt gönderildi Kayıt gönderildi — 10 — (suruyor) — 11 — — — yenilenen bekleyen duyuru: 0, atilgan duyurunun attigi: 0
İlk çalıştırma iki kayıp üretiyor. Süzgeç alanına yazılan her karakter bir duyuru tetiklediği için bekleyen sayı duyurusu bir kez yenileniyor: kullanıcı “12” duyurusunu dinlerken sayı önce beşe, sonra üçe düşüyor ve duyurulan ilk sayı zaten geçersiz. İkinci kayıp daha ağır: atılgan hata bildirimi geldiğinde bekleyen “Kayıt gönderiliyor” duyurusu atılıyor. Kullanıcı, gönderimin başladığını hiç duymadan başarısız olduğunu duyuyor.
İkinci çalıştırma iki düzeltmeyi birlikte uyguluyor. Süzgeç duyurusu her tuşta değil, yazma durduktan sonra bir kez tetikleniyor; bu, Tarayıcı ve Web Platformu kursundaki gecikmeli tetiklemenin erişilebilirlik karşılığıdır. Hata bildirimi de kuyruk boşaldıktan sonraya düşüyor. Kayıp sayısı sıfıra iniyor.
Buradan üç kural çıkar. Kibar bölgeye yazılan metin son durumu bildirmelidir, ara durumları değil. Atılgan öncelik bekleyenleri attığı için yalnızca kullanıcının işini durduran bildirimlerde kullanılır. Bir bölge sık güncelleniyorsa duyuru güncellemeden değil, durulmadan sonra yazılır.
Bölgenin Kurulumu
Canlı bölgenin belgede önceden bulunması gerekir; sonradan eklenen bir bölgenin içeriği bildirilmeyebilir. Boş bir kap sayfa kurulurken konur ve içeriği sonradan doldurulur.
Bölge içeriği kısa ve durum bildiren bir cümle olmalıdır. Tabloyu canlı bölgeye almak —
yani tablonun kendisini aria-live bildirimiyle işaretlemek— her satır değişiminde bütün
içeriğin bildirilmesine yol açar. Doğru kurulum, tablonun dışında bir durum bölgesi
tutmak ve oraya “3 ölçüm listeleniyor” yazmaktır. Tablonun kendisi sessiz kalır; kullanıcı
sayıyı duyduktan sonra tabloya kendi gider.
4.1.3 ölçütü bunu bir gereklilik hâline getirir: odak değiştirmeden sunulan durum iletileri program yoluyla belirlenebilir olmalı ve odak taşımadan bildirilmelidir. Ölçütün iki yönü vardır — ileti bildirilmelidir, ama bildirim için odak çalınmamalıdır. Süzgeç sonucundan sonra odağı tabloya taşıyan kod, ölçütü ters yönden kaldırır.
Özet
- Ekran okuyucu kullanıcısı belgeyi doğrusal okumaz; yer imi, başlık ve bağlantı düzlemleri arasında atlar ve her düzlem belgeden türetilir.
- Düzlem kusurları sayılabilir: adsız yinelenen yer imi, atlanan başlık düzeyi, bağlamdan kopuk bağlantı metni, aynı metinle farklı hedef ve farklı metinle aynı hedef.
- Odak bir ögeye vardığında iletilen bilgi rol, ad, değer, durum ve ek açıklamadan gelir; ada rol adı yazılmaz, uzun metin ada değil açıklamaya yazılır.
- Kibar öncelik duyuruyu sıraya koyar, atılgan öncelik bekleyenleri atarak öne geçer; atılgan bildirim yalnızca işi durduran durumlar içindir.
- Sık güncellenen bir bölgede duyuru her değişimde değil durulmadan sonra yazılır ve son durumu bildirir.
- Canlı bölge belgede önceden bulunmalı, içeriği kısa olmalı ve bildirim odağı taşımamalıdır.
Sonraki Adım
Bu ders bilginin görsel olmayan kanallardan nasıl aktarıldığını kurdu. Aynı bilginin görsel kanaldaki karşılığı ölçülebilir bir eşiğe bağlıdır: ekranda duran metnin ve bileşen sınırlarının çevresinden ayırt edilebilmesi. Sonraki ders karşıtlığı hesaplanabilir bir nicelik olarak ele alır — bağıl parlaklıktan kontrast oranının türetilmesini, metin ve metin dışı ögelerin farklı eşiklerini ve süzgeç panelinin bütün durumlarının bu eşiklere karşı denetlenmesini.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.