İçeriğe geç
academia.sh

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-label yazardan 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat