İçeriğe geç
academia.sh

Ders 10 / 27

Klavye Erişimi

Klavyeyle çalıştırılabilirliğin gerekleri, odak sırasıyla görsel sıranın karşılaştırılması, odak kuşatmasının tuzağa dönüştüğü sınır ve tek karakterli kısayolların koşulu.

İçindekiler

Önceki dersin kural denetimi bir satırı açık bıraktı: düğme rolü bildirilmiş ama odak almayan öge. Bildirim doğruydu, davranış yoktu. Bu ayrım iki farklı ölçütün konusudur ve ikincisi bu dersin çıkış noktasıdır.

Klavye erişimi yalnızca klavye kullanan bir azınlığın sorunu değildir. Ekran okuyucular sayfayı klavye arayüzü üzerinden dolaşır; ses komutuyla ve anahtar denetimiyle çalışan araçlar da aynı arayüzü kullanır. Klavyeyle çalışmayan bir bileşen bu araçların hiçbirinde çalışmaz. Buradan pratik bir sonuç çıkar: klavye denetimi, yardımcı teknoloji denemesinden önce yapılabilecek en ucuz ve en geniş kapsamlı sınamadır.

Ölçüt Ne İster

2.1.1 ölçütü, içeriğin bütün işlevlerinin bir klavye arayüzü üzerinden çalıştırılabilmesini ister. İfadenin iki ayrıntısı önemlidir. Birincisi “klavye arayüzü” denmesi: fiziksel bir klavye değil, o arayüzü kullanan her araç kastedilir. İkincisi, ölçütün tek tek tuş vuruşları için belirli bir zamanlama gerektirmemeyi de istemesidir; belirli bir hızda basılması gereken bir tuş dizisi ölçütü karşılamaz.

Tek istisna, girdinin yolunun kendisinin bilgi taşıdığı işlevlerdir — serbest el çizimi gibi. Bir haritayı sürükleyerek kaydırmak bu istisnaya girmez, çünkü aynı işlev kaydırma düğmeleriyle de sağlanabilir.

2.1.2 ölçütü zincirin diğer ucudur: klavyeyle bir bileşene girilebiliyorsa, aynı arayüzle çıkılabilmelidir. Bu iki ölçüt birlikte bir bütün kurar; birincisi girişi, ikincisi çıkışı güvence altına alır.

Odak Sırası Görsel Sırayla Uyuşmalı

2.4.3 ölçütü, odaklanabilir ögelerin anlamı ve çalıştırılabilirliği koruyan bir sırada gezilmesini ister. Sekme sırası belge sırasından gelir; görsel sıra sunum katmanından gelir. İkisi ayrıştığında klavye kullanıcısı ekranda ileri geri sıçrar.

Ölçüm listesi sayfasında bu ayrışma iki nedenle oluşuyor: süzgeç paneli işaretlemeye sonradan, tablodan sonra eklendi ve sunum katmanı onu tablonun üstüne yerleştirdi; dışa aktarma düğmesine de “önce gelsin” niyetiyle artı bir tabindex değeri yazıldı.

// odaksira.mjs — sekme sirasi ile gorsel siranin karsilastirilmasi (2.4.3)

// gorsel: [satir, sutun] — sunum katmaninin yerlestirdigi yer
const SAYFA = [
  { ad: "a.atlama-bagi", belge: 1, tabindex: null, gorsel: [0, 1] },
  { ad: "button#sirala-tarih", belge: 2, tabindex: null, gorsel: [3, 1] },
  { ad: "button#sirala-sicaklik", belge: 3, tabindex: null, gorsel: [3, 2] },
  { ad: "button.sil[T-01]", belge: 4, tabindex: null, gorsel: [4, 1] },
  { ad: "input#suz-kod", belge: 5, tabindex: null, gorsel: [1, 1] },
  { ad: "input#suz-uyari", belge: 6, tabindex: null, gorsel: [1, 2] },
  { ad: "button#suz", belge: 7, tabindex: null, gorsel: [1, 3] },
  { ad: "button#disa-aktar", belge: 8, tabindex: 1, gorsel: [2, 1] },
];

// M14/K04'te kurulan kural: once artan tabindex, sonra belge sirasi
function sekmeSirasi(ogeler) {
  const pozitif = ogeler.filter((o) => (o.tabindex ?? 0) > 0)
    .sort((a, b) => a.tabindex - b.tabindex || a.belge - b.belge);
  const sifir = ogeler.filter((o) => (o.tabindex ?? 0) === 0 || o.tabindex === null)
    .sort((a, b) => a.belge - b.belge);
  return [...pozitif, ...sifir];
}

const gorselSira = (ogeler) =>
  [...ogeler].sort((a, b) => a.gorsel[0] - b.gorsel[0] || a.gorsel[1] - b.gorsel[1]);

function tersDusenler(ogeler) {
  const sekme = sekmeSirasi(ogeler).map((o) => o.ad);
  const gorsel = gorselSira(ogeler).map((o) => o.ad);
  const ciftler = [];
  for (let i = 0; i < gorsel.length; i++) {
    for (let j = i + 1; j < gorsel.length; j++) {
      if (sekme.indexOf(gorsel[i]) > sekme.indexOf(gorsel[j])) ciftler.push([gorsel[i], gorsel[j]]);
    }
  }
  return ciftler;
}

function rapor(baslik, ogeler) {
  console.log(baslik);
  const sekme = sekmeSirasi(ogeler);
  const gorsel = gorselSira(ogeler);
  console.log("  no  sekme sirasi              gorsel sira");
  for (let i = 0; i < sekme.length; i++) {
    console.log(`  ${String(i + 1).padStart(2)}  ${sekme[i].ad.padEnd(24)}  ${gorsel[i].ad}`);
  }
  const ciftler = tersDusenler(ogeler);
  console.log(`  tersine dusen cift sayisi: ${ciftler.length}`);
  for (const [a, b] of ciftler.slice(0, 4)) {
    console.log(`    "${a}" gorselde once, sekmede "${b}" ogesinden sonra`);
  }
}

rapor("suzgec paneli kaynakta tablodan sonra, tabindex=1 var:", SAYFA);

// Duzeltme: suzgec paneli kaynakta yukari alinir, arti tabindex kaldirilir
const DUZELTILMIS = SAYFA.map((o) => {
  const yeni = { ...o, tabindex: null };
  if (o.ad.startsWith("input#suz") || o.ad === "button#suz") yeni.belge = o.belge - 3;
  if (o.ad === "button#disa-aktar") yeni.belge = 5;
  if (o.ad.startsWith("button#sirala") || o.ad.startsWith("button.sil")) yeni.belge = o.belge + 4;
  return yeni;
});

console.log("");
rapor("suzgec paneli kaynakta yukari alindi, tabindex kaldirildi:", DUZELTILMIS);
suzgec paneli kaynakta tablodan sonra, tabindex=1 var:
  no  sekme sirasi              gorsel sira
   1  button#disa-aktar         a.atlama-bagi
   2  a.atlama-bagi             input#suz-kod
   3  button#sirala-tarih       input#suz-uyari
   4  button#sirala-sicaklik    button#suz
   5  button.sil[T-01]          button#disa-aktar
   6  input#suz-kod             button#sirala-tarih
   7  input#suz-uyari           button#sirala-sicaklik
   8  button#suz                button.sil[T-01]
  tersine dusen cift sayisi: 13
    "a.atlama-bagi" gorselde once, sekmede "button#disa-aktar" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#disa-aktar" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#sirala-tarih" ogesinden sonra
    "input#suz-kod" gorselde once, sekmede "button#sirala-sicaklik" ogesinden sonra

suzgec paneli kaynakta yukari alindi, tabindex kaldirildi:
  no  sekme sirasi              gorsel sira
   1  a.atlama-bagi             a.atlama-bagi
   2  input#suz-kod             input#suz-kod
   3  input#suz-uyari           input#suz-uyari
   4  button#suz                button#suz
   5  button#disa-aktar         button#disa-aktar
   6  button#sirala-tarih       button#sirala-tarih
   7  button#sirala-sicaklik    button#sirala-sicaklik
   8  button.sil[T-01]          button.sil[T-01]
  tersine dusen cift sayisi: 0

Sekiz ögelik bir sayfada on üç ters çift oluşuyor. Sayı, iki hatanın çarpımıdır: artı tabindex değeri tek başına yedi çift üretir, kaynak sırasındaki yanlış yerleşim kalanı. İkinci rapor düzeltmenin yönünü gösteriyor — sıra, sunum katmanında değil kaynakta düzeltilir. Görsel sırayı sunum katmanı belirlediği için, kaynağı ona uydurmaktan başka tutarlı bir yol yoktur.

Ters çift sayısı bir denetim ölçüsü olarak kullanılabilir: sıfırdan büyük her değer, en az bir kullanıcının ekranda geriye sıçradığı anlamına gelir. Ölçüm tablosunun her satırındaki silme düğmesi sekme sırasını uzatıyorsa, Tarayıcı ve Web Platformu kursunda kurulan dönen sekme durağı düzeni grubu tek durağa indirir; bu, sırayı bozmadan uzunluğu düşürür.

Odak Kuşatması ve Tuzak

Ölçüm giriş formu bir pencere olarak açıldığında arkasındaki sayfa erişilebilir kalmamalıdır: sekme tuşu pencerenin içinde dönmelidir. Bu düzene odak kuşatması denir ve yanlış kurulduğunda 2.1.2 ölçütünü kaldıran bir tuzağa dönüşür. Aşağıdaki betik üç kurulumu bir durum makinesi olarak modelliyor ve her birinden hangi durumların erişilebilir olduğunu tarayarak denetliyor.

// tuzak.mjs — odak kusatmasinin durum makinesi ve klavye tuzagi denetimi (2.1.2)

const ARKA = ["input#suz-kod", "button#suz", "button#yeni-olcum", "button.sil[T-01]"];
const PENCERE = ["input#tarih", "input#deger", "button#kaydet", "button#kapat"];
const TUSLAR = ["Tab", "Shift+Tab", "Escape"];

// tur: "kusatmasiz" | "cikissiz" | "dogru"
function makine(tur) {
  const cikisVar = tur !== "cikissiz";
  return (d, tus) => {
    if (!d.acik) {
      const i = ARKA.indexOf(d.odak);
      if (tus === "Tab") return { acik: false, odak: ARKA[(i + 1) % ARKA.length] };
      if (tus === "Shift+Tab") return { acik: false, odak: ARKA[(i + ARKA.length - 1) % ARKA.length] };
      return d;
    }
    const halka = tur === "kusatmasiz" ? [...PENCERE, ...ARKA] : PENCERE;
    const i = halka.indexOf(d.odak);
    if (tus === "Tab") return { acik: true, odak: halka[(i + 1) % halka.length] };
    if (tus === "Shift+Tab") return { acik: true, odak: halka[(i + halka.length - 1) % halka.length] };
    if (tus === "Escape" && cikisVar) return { acik: false, odak: "button#yeni-olcum" };
    return d;
  };
}

const anahtar = (d) => (d.acik ? "acik" : "kapali") + "|" + d.odak;

function denetle(tur) {
  const sonraki = makine(tur);
  const baslangic = { acik: true, odak: PENCERE[0] };
  const gorulen = new Map([[anahtar(baslangic), baslangic]]);
  const kuyruk = [baslangic];
  while (kuyruk.length > 0) {
    const d = kuyruk.shift();
    for (const tus of TUSLAR) {
      const y = sonraki(d, tus);
      if (gorulen.has(anahtar(y))) continue;
      gorulen.set(anahtar(y), y);
      kuyruk.push(y);
    }
  }
  const durumlar = [...gorulen.values()];
  return {
    ulasilan: durumlar.length,
    kacak: durumlar.some((d) => d.acik && ARKA.includes(d.odak)),
    cikis: durumlar.some((d) => !d.acik),
  };
}

console.log("kurulum       ulasilan durum  arka plana kacak  klavyeyle cikis  2.1.2");
for (const tur of ["kusatmasiz", "cikissiz", "dogru"]) {
  const s = denetle(tur);
  console.log(
    tur.padEnd(14) +
      String(s.ulasilan).padStart(14) +
      (s.kacak ? "  var" : "  yok").padEnd(18) +
      (s.cikis ? " var" : " yok").padEnd(17) +
      (s.cikis ? "gecti" : "KALDI"),
  );
}

// Dogru kurulumda tus dizisi
console.log("\ndogru kurulumda odak izi:");
let d = { acik: true, odak: PENCERE[0] };
const sonraki = makine("dogru");
console.log("  " + "baslangic".padEnd(18) + " " + "pencere acik".padEnd(14) + " odak: " + d.odak);
for (const tus of ["Tab", "Tab", "Tab", "Tab", "Shift+Tab", "Escape", "Tab"]) {
  d = sonraki(d, tus);
  console.log(`  ${tus.padEnd(18)} ${(d.acik ? "pencere acik" : "pencere kapali").padEnd(14)} odak: ${d.odak}`);
}
kurulum       ulasilan durum  arka plana kacak  klavyeyle cikis  2.1.2
kusatmasiz                12  var              var             gecti
cikissiz                   4  yok              yok             KALDI
dogru                      8  yok              var             gecti

dogru kurulumda odak izi:
  baslangic          pencere acik   odak: input#tarih
  Tab                pencere acik   odak: input#deger
  Tab                pencere acik   odak: button#kaydet
  Tab                pencere acik   odak: button#kapat
  Tab                pencere acik   odak: input#tarih
  Shift+Tab          pencere acik   odak: button#kapat
  Escape             pencere kapali odak: button#yeni-olcum
  Tab                pencere kapali odak: button.sil[T-01]

Üç satır üç ayrı kusuru ayırıyor. Kuşatmasız kurulum ölçütü geçiyor — çıkış vardır — ama pencere açıkken arka plandaki dört durağa odak kaçıyor. Kullanıcı görmediği, üstü örtülü ögelere odaklanır; bu bir uygunluk ihlali değil, kullanımı bozan bir kusurdur. Çıkışsız kurulum yalnızca dört duruma ulaşıyor: pencerenin dışına çıkan hiçbir yol yok. Bu, tanımı gereği klavye tuzağıdır ve 2.1.2 ölçütünü kaldırır. Doğru kurulumda sekiz durumun tamamına ulaşılıyor, kaçak yok ve çıkış var.

Odak izinin son iki satırı ayrı bir kuralı gösteriyor: pencere kapandığında odak, pencereyi açan düğmeye döner. Dönmezse odak belgenin köküne düşer ve kullanıcı dolaşıma en baştan başlar. Bu davranış Tarayıcı ve Web Platformu kursunda odağı program taşıdığında doğan sorumluluk olarak kurulmuştu; burada ölçütün gereği hâline geliyor.

Kuşatmanın kendisi de tek bir yolla kurulmaz. Sekme tuşunu yakalayıp odağı halkada döndürmek bir yoldur; pencere dışındaki içeriği ağaçtan gizlemek ve etkileşim dışı bırakmak diğeridir. İkincisi yeğlenir, çünkü kuşatmayı yalnızca sekme tuşuna değil, ekran okuyucunun kendi gezinme komutlarına karşı da kurar — sekme tuşu yakalanmış bir pencerede kullanıcı yer imi listesinden arka plana geçebilir.

Odağın Görünürlüğü

Klavyeyle çalışan bir arayüzde odağın nerede olduğu her an görünmelidir; 2.4.7 ölçütü bunu ister. Odak göstergesini sunum katmanından kaldırmak, ölçütü tek satırda kaldırır. Gösterge değiştirilebilir: kalınlığı, rengi ve biçimi tasarım kararıdır. Kaldırılamaz.

İki ek koşul vardır. Gösterge, komşu renklerden ayırt edilebilecek kadar karşıtlık taşımalıdır — bu, metin dışı kontrast ölçütünün konusudur ve karşıtlık dersinde hesaplanacaktır. Odaklanan öge ayrıca ekranda örtülü kalmamalıdır: sayfanın altına yapışan bir çubuk ya da üstte duran bir başlık, odaklanan ögeyi tamamen kapatıyorsa gösterge var ama görünmez demektir.

Kısayollar

Ölçüm listesinde süzgeç alanına atlamak için tek harflik bir kısayol yazmak akla yakın gelir. 2.1.4 ölçütü bu tür kısayollara üç koşuldan en az birini dayatır: kısayol kapatılabilmeli, kullanıcı tarafından bir değiştirici tuşla birlikte kullanılacak biçimde yeniden atanabilmeli, ya da yalnızca ilgili bileşen odaktayken etkin olmalıdır.

Gerekçe iki kullanım biçiminde görünür. Sesle yazan bir kullanıcının çevresindeki konuşma metne çevrilebilir ve tek harfler kısayol olarak yorumlanır; klavyede tek tuşa basmakta zorlanan bir kullanıcı istemeden kısayol tetikler. Ölçüm giriş formunda bu, açıklama alanına yazarken satırların silinmesi anlamına gelir.

Uygulanabilir karar şudur: tek karakterli kısayollar belge geneline değil, odaktaki bileşene bağlanır. Ölçüm tablosunda ok tuşlarıyla satır gezinmesi tablo odaktayken çalışır; tablo odakta değilken aynı tuşlar sayfayı kaydırır. Belge geneline bağlanan kısayollar bir değiştirici tuşla yazılır ve kullanıcıya kapatma yolu bırakılır.

Özet

  • 2.1.1 bütün işlevlerin klavye arayüzünden ve tuş vuruşu zamanlamasına bağlı olmadan çalıştırılmasını ister; 2.1.2 girilen her yerden aynı arayüzle çıkılmasını ister.
  • Sekme sırası kaynaktan, görsel sıra sunum katmanından gelir; ayrışma tersine düşen çiftler üretir ve düzeltme sunum katmanında değil kaynakta yapılır.
  • Artı tabindex değerleri bütün sıranın önüne geçerek ters çift sayısını tek başına yükseltir.
  • Odak kuşatması üç kurulumda farklı sonuç verir: kuşatmasız kurulum arka plana kaçak bırakır, çıkışsız kurulum klavye tuzağıdır, doğru kurulumda kapanışta odak açan denetime döner.
  • Odak göstergesi biçim olarak değiştirilebilir ama kaldırılamaz; ayrıca yeterli karşıtlık taşımalı ve başka bir ögeyle örtülmemelidir.
  • Tek karakterli kısayollar kapatılabilir, yeniden atanabilir ya da yalnızca bileşen odaktayken etkin olmalıdır.

Sonraki Adım

Bu ders odağın nereye gittiğini hesapladı ama odak oraya vardığında kullanıcının ne öğrendiğini sormadı. Pencere açıldığında yeni bir bağlamın açıldığı nasıl bildirilir? Süzgeç uygulandığında tabloda kaç satır kaldığı, ekranı görmeyen bir kullanıcıya nasıl ulaşır? Sonraki ders duyuru düzeneklerini ele alır: canlı bölgelerin kibar ve atılgan ayrımını, duyuruların kuyruklanmasını ve yer imleriyle başlıkların kurduğu gezinme düzlemlerini.

İ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