Ders 09 / 27
ARIA Kullanımı
Erişilebilirlik ağacına yazılan rol, durum ve özellik bildirimleri; ad hesabının tamamı, birinci kuralın gerekçesi ve kötüye kullanımın ürettiği somut kırılmalar.
İçindekiler
Önceki ders yerel ögelerin ağaca ne bildirdiğini kurdu ve bir boşluk bıraktı: tanımlı karşılığı olmayan bileşenler. Süzgeç paneli açılıp kapanır, ölçüm tablosunun sütunları sıralanabilir, uyarı bildirimleri sonradan belirir. Bu davranışların hiçbirinin tek bir öge karşılığı yoktur; ağaçta doğru görünmeleri için eksik bildirimlerin yazılması gerekir.
Bu bildirimleri yazan küme ARIA’dır. Aynı küme, en sık yanlış kullanılan araçtır ve yanlış kullanıldığında arayüzü hiç bildirim yazılmamış hâlinden daha kullanılmaz yapar. Ders iki şeyi birlikte kurar: kümenin ne yaptığını ve neyi bozduğunu.
ARIA Neyi Değiştirir
ARIA bildirimleri yalnızca erişilebilirlik ağacını değiştirir. Bir öznitelik yazmak o ögeye davranış eklemez: odaklanabilir yapmaz, tuş dinlemez, tıklamayı işlemez, görünümünü değiştirmez.
Bu tek cümle yanlış kullanımların çoğunun kaynağını açıklar. Bir div ögesine düğme rolü
yazan kod, ekranda düğme gibi görünen ve ağaçta düğme olarak bildirilen ama boşluk tuşuna
yanıt vermeyen bir şey üretir. Bildirim ile davranış ayrıştığında ortaya çıkan durum,
bildirim hiç yazılmamış olmasından kötüdür: kullanıcıya düğme olduğu söylenmiş, kullanma
yolu verilmemiştir.
Rol, Durum, Özellik
Küme üç tür bildirim taşır.
Rol, ögenin ne olduğunu söyler ve örtük rolün yerine geçer. Bir kez yazılır, çalışma sırasında değişmesi beklenmez.
Durum (state), ögenin o andaki hâlidir ve etkileşimle değişir: genişletilmiş mi
(aria-expanded), işaretli mi (aria-checked), seçili mi (aria-selected), meşgul mü
(aria-busy), geçersiz mi (aria-invalid).
Özellik (property), ögenin daha kalıcı niteliğidir: adı (aria-label,
aria-labelledby), açıklaması (aria-describedby), neyi denetlediği (aria-controls),
zorunlu olup olmadığı (aria-required), hangi sırayla sıralandığı (aria-sort).
Ayrım biçimsel değildir: durum bildirimleri değiştikçe güncellenmek zorundadır. Panel
açıldığında aria-expanded değeri güncellenmiyorsa ağaç, ekranda görünenle çelişen bir
bilgi taşır. Yanlış bilgi, eksik bilgiden daha maliyetlidir.
Birinci Kural
ARIA’nın yazım kurallarının birincisi şudur: gereken anlamı taşıyan yerel bir öge varsa
ARIA kullanılmaz. div ögesine düğme rolü yazmak yerine button yazılır; liste rolü
yerine ul kullanılır; başlık rolü yerine başlık ögesi kullanılır.
Gerekçesi önceki dersteki sayımdır. Yerel öge rolü, adı, odaklanabilirliği, tuş davranışını, durum bildirimlerini ve form ilişkisini birlikte getirir; rol bildirimi yalnızca birincisini getirir. Kalan beşinin elle yazılması hem daha uzun hem de her gerçekleştirimde yeniden hata yapılabilir bir iştir.
Kalan kurallar bu birincinin uzantısıdır: yerel bir ögenin anlamı zorunlu olmadıkça değiştirilmez; rol yazılan her etkileşimli öge klavyeyle çalıştırılabilir olmalıdır; odaklanabilir bir öge ağaçtan gizlenmez; etkileşimli her ögenin bir erişilebilir adı olmalıdır.
Ad Hesabının Tamamı
Önceki dersin ad hesabı yerel kaynaklarla sınırlıydı. Sıranın en üstünde ARIA kaynakları durur ve yerel her şeyi bastırır. Aşağıdaki betik tam sırayı uygular ve aynı işaretleme kümesi üzerinde kural denetimi çalıştırır.
// aria.mjs — ad hesabinin tamami (ARIA kaynaklari en ustte) ve kotuye kullanim denetimi const ETKILESIMLI = new Set(["button", "link", "checkbox", "textbox", "searchbox", "spinbutton"]); const ICERIKTEN = new Set(["button", "link", "heading", "cell", "columnheader", "checkbox", "listitem"]); const ADA_KAPALI = new Set(["generic", "presentation", "none", "paragraph"]); // yazardan ad almaz function rol(d) { const o = d.oz ?? {}; if (o.role) return o.role; switch (d.oge) { case "a": return o.href === undefined ? "generic" : "link"; case "button": return "button"; case "input": return o.type === "checkbox" ? "checkbox" : o.type === "search" ? "searchbox" : "textbox"; case "table": return "table"; case "section": return o["aria-label"] || o["aria-labelledby"] ? "region" : "generic"; case "h2": return "heading"; default: return "generic"; } } const duz = (s) => s.replace(/\s+/g, " ").trim(); // Icerik metni; aria-hidden alt agaci atlanir function metin(d, gorunur = true) { if (typeof d === "string") return d; const o = d.oz ?? {}; if (gorunur && (o["aria-hidden"] === true || o.hidden)) return ""; if (d.oge === "img") return o.alt ?? ""; return duz((d.cocuklar ?? []).map((c) => metin(c, gorunur)).join(" ")); } // 1 aria-labelledby -> 2 aria-label -> 3 yerel kaynak -> 4 icerik -> 5 son care function ad(d, belge) { const o = d.oz ?? {}; const r = rol(d); const yazardanAlir = !ADA_KAPALI.has(r); if (yazardanAlir && o["aria-labelledby"]) { const parcalar = o["aria-labelledby"].split(" ").map((k) => belge[k]).filter(Boolean); // basvurulan dugum gizli olsa da metni kullanilir if (parcalar.length > 0) return [duz(parcalar.map((p) => metin(p, false)).join(" ")), "aria-labelledby"]; } if (yazardanAlir && o["aria-label"]) return [duz(o["aria-label"]), "aria-label"]; if (["input", "select", "textarea"].includes(d.oge) && belge["label:" + o.id]) { return [metin(belge["label:" + o.id]), "label ogesi"]; } if (ICERIKTEN.has(r)) { const m = metin(d); if (m) return [m, "icerik metni"]; } return ["", "ad yok"]; } // --- Kuzey Yamac: suzgec paneli ve olcum tablosu bilesenleri ----------------- const BELGE = { "baslik": { oge: "h2", cocuklar: ["Süzgeç"] }, "sayi": { oge: "span", cocuklar: ["3 ölçüt"] }, "gizli-ad": { oge: "span", oz: { hidden: true }, cocuklar: ["Ölçüm satırını sil"] }, "label:kod": { oge: "label", cocuklar: ["İstasyon kodu"] }, }; const ORNEK = [ ['<button>Süz</button>', { oge: "button", cocuklar: ["Süz"], odaklanir: true }], ['<button aria-label="Kaydet">Ölçümü sil</button>', { oge: "button", oz: { "aria-label": "Kaydet" }, cocuklar: ["Ölçümü sil"], odaklanir: true }], ['<button aria-labelledby="baslik sayi">Aç</button>', { oge: "button", oz: { "aria-labelledby": "baslik sayi" }, cocuklar: ["Aç"], odaklanir: true }], ['<button aria-labelledby="yok-boyle">Sil</button>', { oge: "button", oz: { "aria-labelledby": "yok-boyle" }, cocuklar: ["Sil"], odaklanir: true }], ['<div role="button" tabindex="0">Sil</div>', { oge: "div", oz: { role: "button" }, cocuklar: ["Sil"], odaklanir: true }], ['<div role="button">Sil</div>', { oge: "div", oz: { role: "button" }, cocuklar: ["Sil"], odaklanir: false }], ['<input id="kod" aria-label="Kod"> + label', { oge: "input", oz: { id: "kod", "aria-label": "Kod" }, odaklanir: true }], ['<div aria-label="Süzgeç paneli">…</div>', { oge: "div", oz: { "aria-label": "Süzgeç paneli" }, cocuklar: ["3 ölçüt"], odaklanir: false }], ['<section aria-label="Süzgeç paneli">…', { oge: "section", oz: { "aria-label": "Süzgeç paneli" }, cocuklar: ["3 ölçüt"], odaklanir: false }], ['<button aria-hidden="true">Dışa aktar</button>', { oge: "button", oz: { "aria-hidden": true }, cocuklar: ["Dışa aktar"], odaklanir: true }], ['<button aria-labelledby="gizli-ad">×</button>', { oge: "button", oz: { "aria-labelledby": "gizli-ad" }, cocuklar: ["×"], odaklanir: true }], ['<table role="presentation">…</table>', { oge: "table", oz: { role: "presentation" }, cocuklar: ["Sıcaklık"], odaklanir: false }], ]; console.log("isaretleme".padEnd(51) + "rol".padEnd(14) + "erisilebilir ad".padEnd(22) + "kaynak"); for (const [kaynak, d] of ORNEK) { const [a, nereden] = ad(d, BELGE); console.log(kaynak.padEnd(51) + rol(d).padEnd(14) + (a === "" ? "(bos)" : '"' + a + '"').padEnd(22) + nereden); } console.log("\nkural denetimi:"); let sayi = 0; for (const [kaynak, d] of ORNEK) { const o = d.oz ?? {}; const r = rol(d); const [a] = ad(d, BELGE); const gorunen = metin(d); const bulgular = []; if (o["aria-hidden"] === true && d.odaklanir) bulgular.push("odaklanabilir oge agactan gizlendi"); if (o["aria-labelledby"] && !o["aria-labelledby"].split(" ").some((k) => BELGE[k])) bulgular.push("aria-labelledby gecersiz kimlige basvuruyor"); if ((o["aria-label"] || o["aria-labelledby"]) && ADA_KAPALI.has(r)) bulgular.push("yazardan ad almayan rolde ad bildirimi yok sayildi"); if (ETKILESIMLI.has(r) && !d.odaklanir) bulgular.push("etkilesimli rol odaklanabilir degil"); if (ETKILESIMLI.has(r) && a === "") bulgular.push("etkilesimli oge adsiz"); if (["presentation", "none"].includes(o.role) && ["table", "ul", "ol", "li"].includes(d.oge)) bulgular.push("yerel yapi anlami kaldirildi: satir ve sutun iliskileri agacta yok"); if (ETKILESIMLI.has(r) && /\p{L}/u.test(gorunen) && a && !a.toLocaleLowerCase("tr").includes(gorunen.toLocaleLowerCase("tr"))) bulgular.push('gorunen metin ("' + gorunen + '") erisilebilir adin icinde yok'); for (const b of bulgular) { sayi++; console.log(" " + kaynak.padEnd(51) + b); } } console.log(`\n${ORNEK.length} ornekte ${sayi} bulgu`);
isaretleme rol erisilebilir ad kaynak
<button>Süz</button> button "Süz" icerik metni
<button aria-label="Kaydet">Ölçümü sil</button> button "Kaydet" aria-label
<button aria-labelledby="baslik sayi">Aç</button> button "Süzgeç 3 ölçüt" aria-labelledby
<button aria-labelledby="yok-boyle">Sil</button> button "Sil" icerik metni
<div role="button" tabindex="0">Sil</div> button "Sil" icerik metni
<div role="button">Sil</div> button "Sil" icerik metni
<input id="kod" aria-label="Kod"> + label textbox "Kod" aria-label
<div aria-label="Süzgeç paneli">…</div> generic (bos) ad yok
<section aria-label="Süzgeç paneli">… region "Süzgeç paneli" aria-label
<button aria-hidden="true">Dışa aktar</button> button (bos) ad yok
<button aria-labelledby="gizli-ad">×</button> button "Ölçüm satırını sil" aria-labelledby
<table role="presentation">…</table> presentation (bos) ad yok
kural denetimi:
<button aria-label="Kaydet">Ölçümü sil</button> gorunen metin ("Ölçümü sil") erisilebilir adin icinde yok
<button aria-labelledby="baslik sayi">Aç</button> gorunen metin ("Aç") erisilebilir adin icinde yok
<button aria-labelledby="yok-boyle">Sil</button> aria-labelledby gecersiz kimlige basvuruyor
<div role="button">Sil</div> etkilesimli rol odaklanabilir degil
<div aria-label="Süzgeç paneli">…</div> yazardan ad almayan rolde ad bildirimi yok sayildi
<button aria-hidden="true">Dışa aktar</button> odaklanabilir oge agactan gizlendi
<button aria-hidden="true">Dışa aktar</button> etkilesimli oge adsiz
<table role="presentation">…</table> yerel yapi anlami kaldirildi: satir ve sutun iliskileri agacta yok
12 ornekte 8 bulgu
Betikteki odaklanir alanı, ögenin gerçekten odak alıp almadığını temsil eder; div
satırlarında tabindex yazılıp yazılmamasının karşılığıdır.
Bulguların Okunması
Ad bildirimi görünen metni bastırır. İkinci satırda düğmenin üzerinde “Ölçümü sil”
yazıyor, ağaçta adı “Kaydet”. Konuşarak arayüz kullanan bir kişi gördüğü metni söyler ve
söylediği ad hiçbir ögeye uymaz. 2.5.3 ölçütü tam da bunu yasaklar: görünen etiket metni,
erişilebilir adın içinde geçmelidir. Aynı kırılma yedinci satırda daha sinsi biçimde
oluşuyor — alanın bir label ögesi var, ama aria-label bildirimi onu bastırdığı için
ekranda “İstasyon kodu” yazan alanın adı “Kod”.
Geçersiz başvuru sessizce düşer. Dördüncü satırda aria-labelledby var olmayan bir
kimliğe işaret ediyor; hesap o adımı atlıyor ve ad içerik metninden geliyor. Sonuç bu
örnekte zararsız görünür, ancak asıl bildirim yok sayılmıştır. İçeriği olmayan bir simge
düğmesinde aynı hata adsız bir düğme bırakır.
Gizli düğüme yapılan başvuru geçerlidir. On birinci satır bunu gösteriyor: gizlenmiş
bir span içindeki metin, ona başvuran düğmenin adı oluyor. Ekranda yalnız bir çarpı
işareti duran silme düğmesine ad vermenin tanımlı yolu budur.
Yazardan ad almayan roller vardır. Sekizinci satırda bir div ögesine yazılan
aria-label yok sayılıyor; dokuzuncu satırda aynı bildirim section ögesine yazıldığında
hem ögenin rolünü bölge yapıyor hem de ad üretiyor. Ad bildirimi ögeye değil, ögenin
rolüne yazılır.
Odaklanabilir bir öge ağaçtan gizlenemez. Onuncu satır iki bulgu birden üretiyor: düğme odak alıyor ama ağaçta yok. Klavyeyle gezen bir kullanıcı odağın kaybolduğu bir durakla karşılaşır; sekme tuşu bir yere gider, orada hiçbir şey bildirilmez.
Yapı anlamını kaldırmak veriyi kaldırır. Son satırda tablonun rolü sunumsala çevrilmiş. Düzen amacıyla kullanılan bir tabloda bu doğru karardır; veri taşıyan ölçüm tablosunda satır ve sütun ilişkileri ağaçtan tamamen düşer ve hücreler arasında dolaşan kullanıcı hangi sütunda olduğunu öğrenemez.
Süzgeç Panelinde Doğru Kullanım
Aşağıdaki işaretleme, yerel ögeyle karşılanmayan iki yapıyı kuralına uygun bildirir: açılıp kapanan panel ve sıralanabilir sütun.
<h2 id="suzgec-basligi">Süzgeç</h2> <button aria-expanded="false" aria-controls="suzgec-govde"> Süzgeç ölçütleri </button> <div id="suzgec-govde" hidden> <label for="suz-kod">İstasyon kodu</label> <input id="suz-kod" type="search"> <p><label><input type="checkbox" name="uyari"> Yalnız uyarılı ölçümler</label></p> </div> <table aria-labelledby="tablo-basligi"> <caption id="tablo-basligi">Günlük ölçümler</caption> <thead> <tr> <th scope="col" aria-sort="descending"> <button>Tarih</button> </th> <th scope="col" aria-sort="none"><button>Sıcaklık</button></th> </tr> </thead> </table>
Üç karar dikkat gerektirir. Açma denetimi bir button ögesidir; rol yazılmamıştır, çünkü
gereken rol zaten vardır. aria-expanded durumu panel her açılıp kapandığında
güncellenmelidir; sabit kalırsa ağaç yanlış bilgi taşır. Sıralama durumu başlık hücresine
yazılır, hücrenin içindeki düğmeye değil: sıralanan şey sütundur, düğme yalnızca sıralamayı
başlatan denetimdir.
Özet
- ARIA bildirimleri yalnızca erişilebilirlik ağacını değiştirir; davranış, odaklanabilirlik ya da görünüm eklemez.
- Rol ögenin ne olduğunu, durum o andaki hâlini, özellik daha kalıcı niteliğini bildirir; durum bildirimleri etkileşimle birlikte güncellenmek zorundadır.
- Birinci kural, gereken anlamı taşıyan yerel öge varken ARIA kullanmamaktır; yerel öge rolle birlikte ad, odak, tuş davranışı ve durumları da getirir.
- Ad hesabında ARIA kaynakları yerel kaynakları bastırır; görünen metinle erişilebilir adın ayrışması 2.5.3 ölçütünü kaldırır.
- Geçersiz kimlik başvurusu sessizce düşer, gizli düğüme yapılan başvuru geçerlidir ve
aria-labelyazardan ad almayan rollerde yok sayılır. - Odaklanabilir bir ögeyi ağaçtan gizlemek ve veri tablosunun yapı anlamını kaldırmak, bildirim yazmamaktan daha ağır kırılmalar üretir.
Sonraki Adım
Kural denetiminin bir satırı bu derste açılmadan bırakıldı: düğme rolü yazılmış ama odak almayan öge. Bildirimin doğru olması yetmez, ögenin klavyeyle çalıştırılabilir olması da gerekir ve bu iki gereklilik ayrı ölçütlerde tanımlıdır. Sonraki ders arayüzün tamamına klavyeden bakar: hangi ögelerin sekme sırasında bulunduğunu, sıranın nasıl hesaplandığını, açılan bir panelin odağı nasıl kuşattığını ve kuşatmanın hangi noktada tuzağa dönüştüğünü ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.