İçeriğe geç
academia.sh

Ders 04 / 25

Açılır Listeler

Yerel seçim ögesiyle elle kurulan birleşik giriş alanının karşılaştırılması; etkin seçenek bildiriminin odaktan ayrılması, klavye sözleşmesinin sınanması ve ulaşılan durumlarda değişmezlerin denetimi.

İçindekiler

Seçenek grubu, seçenekler ekrana sığdığında çalışır. Katalogun süzgeç panelindeki “Konu” alanı iki yüz elliyi aşkın değer taşır; hepsi ekranda gösterilemez ve gösterilse de gezilemez. Bu, seçim işini kapanabilen bir listeye devretmeyi gerektirir.

Kalıbın iki gerçekleştirimi vardır ve aralarındaki fark tek bir satırla özetlenemez: biri tek bir ögeyle gelir, diğeri on ayrı bildirimi elle yazmayı gerektirir. Bu ders ikisini de belirtir ve ikincisinin bedelini sayar.

Kalıbın Çözdüğü Sorun

Açılır liste, sayılabilir ama kalabalık bir seçenek kümesinden tek bir değer seçtirir. İki sınırı vardır. Alt sınırda, beşten az seçenek varsa liste gereksizdir: dört ödünç süresi seçeneği bir seçenek grubuyla ekranda durur ve kullanıcı hepsini bir bakışta görür. Üst sınırda, seçenekler kullanıcının önceden bilmediği bir kümeden geliyorsa — kataloğun tamamındaki yazar adları gibi — liste değil arama gerekir.

Ayrıca liste, birden çok değerin seçileceği yerlerde de doğru bileşen değildir. Çok seçimli açılır liste kalıbı kullanıcıya seçtiklerinin tamamını aynı anda göstermez; onun yerine onay kutusu grubu ya da seçilenleri ayrı gösteren bir kalıp kullanılır.

Yerel Öge Önce

Karşılık select ögesidir. Getirdikleri şunlardır: birleşik giriş alanı rolü, label ögesinden hesaplanan ad, açık/kapalı durumunun bildirimi, seçili seçeneğin değer olarak bildirimi, optgroup ile gruplama, klavye sözleşmesinin tamamı ve yazarak arama.

Bir bedeli vardır ve belirtimde yazılmalıdır: açılan seçenek listesinin görünümü sunum katmanından denetlenemez. Liste, sayfanın çizim düzleminin dışında oluşturulur; yazı tipi, renk, satır yüksekliği ve seçenek içine konacak ek bilgi tasarım kararına açık değildir.

Katalogun “Konu” alanında bu bedel ödenebilir. Sonuç sıralama ölçütü, ödünç süresi ve sayfa büyüklüğü seçicilerinin tamamı yerel ögeyle karşılanır. Elle kurulan bir bileşen yalnızca üç gereksinimden biri varsa seçilir: seçenek satırında ek bilgi gösterilmesi (kayıt sayısı, alt başlık), yazarak süzme, ya da seçeneklerin sunucudan aramayla getirilmesi.

Özel Bileşenin Bedeli

Elle kurulan bileşende yeniden üretilmesi gereken bildirimler şunlardır: rol (role="combobox"), açık/kapalı durumu (aria-expanded), denetlenen listenin kimliği (aria-controls), liste rolü (role="listbox"), seçenek rolleri (role="option"), etkin seçeneğin bildirimi (aria-activedescendant), seçili seçeneğin bildirimi (aria-selected), otomatik tamamlama biçimi (aria-autocomplete), klavye sözleşmesinin tamamı ve süzme sonucunun duyurulması.

<label for="konu">Konu</label>
<input id="konu" role="combobox" aria-expanded="true" aria-controls="konu-liste"
       aria-activedescendant="konu-2" aria-autocomplete="list" autocomplete="off">
<ul id="konu-liste" role="listbox" aria-label="Konu seçenekleri">
  <li id="konu-1" role="option" aria-selected="false">Mimarlık</li>
  <li id="konu-2" role="option" aria-selected="true">Mikrobiyoloji</li>
</ul>
<p role="status">2 konu listeleniyor</p>

Bu işaretlemenin ayırt edici yeri aria-activedescendant özniteliğidir. Klavye odağı alandan hiç çıkmaz; listede gezinen şey odak değil, bir bildirimdir. Kullanıcı ok tuşuna bastığında alandaki imleç yerinde kalır, ağaçta ise başka bir seçenek etkin olarak duyurulur. Bu ayrım, alanın metin düzenleme davranışının korunmasını sağlar; odak gerçekten listeye taşınsaydı yazmaya devam etmek olanaksız olurdu.

Ayrımın tasarıma yansıyan sonucu, iki ayrı göstergenin gerekmesidir: alanda odak halkası, listede etkin seçenek vurgusu. İkisinden biri eksikse kullanıcı ya nerede olduğunu ya neyin seçileceğini göremez.

Klavye Sözleşmesi

Tuş Davranış
Tab Alan tek sekme durağıdır; liste açıkken de odak alandadır. Liste kapanır ve odak dışarı çıkar.
Aşağı ok Liste kapalıysa açar ve ilk seçeneği etkinleştirir; açıksa bir sonrakine geçer.
Yukarı ok Liste kapalıysa açar ve son seçeneği etkinleştirir; açıksa bir öncekine geçer.
Enter Etkin seçeneği değer yapar ve listeyi kapatır.
Escape Liste açıksa kapatır ve değeri korur; kapalıysa alanı temizler.
Yazı tuşları Listeyi açar ve seçenekleri süzer.
Home / End İmleci metin içinde taşır; listeye ait değildir.

Ok tuşlarının uçlarda başa dönmemesi, seçenek grubunun sözleşmesinden ayrıldığı noktadır. Seçenek grubunda dönmek kullanıcıyı kaybettirmez, çünkü grubun tamamı ekrandadır. Uzun bir listede dönmek, kullanıcıyı bir tuş vuruşuyla listenin öbür ucuna atar ve nerede olduğunu okumadan anlamasını olanaksız kılar.

Aşağıdaki sınama sözleşmeyi bir durum makinesi olarak kurar, tuş dizileriyle denetler ve son sınamada ulaşılabilen bütün durumları tarayarak üç değişmezi doğrular.

// acilir.test.mjs — birlesik giris alaninin klavye sozlesmesi ve degismezleri
// Kosum: node --test acilir.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";

const KONULAR = ["Mimarlık", "Mikrobiyoloji", "Müzik Tarihi", "Matematik"];
const kucuk = (s) => s.toLocaleLowerCase("tr");

const baslangic = () => ({ odakAlanda: true, acik: false, etkin: null, metin: "", deger: null });
const secenekler = (d) => KONULAR.filter((k) => kucuk(k).startsWith(kucuk(d.metin)));

function tus(d, t) {
  const s = secenekler(d);
  if (t === "ArrowDown") {
    if (!d.acik) return { ...d, acik: true, etkin: s.length ? 0 : null };
    if (d.etkin === null) return { ...d, etkin: s.length ? 0 : null };
    return { ...d, etkin: Math.min(d.etkin + 1, s.length - 1) };   // dongu yok
  }
  if (t === "ArrowUp") {
    if (!d.acik) return { ...d, acik: true, etkin: s.length ? s.length - 1 : null };
    if (d.etkin === null) return { ...d, etkin: s.length ? s.length - 1 : null };
    return { ...d, etkin: Math.max(d.etkin - 1, 0) };
  }
  if (t === "Enter") {
    if (d.acik && d.etkin !== null)
      return { ...d, acik: false, etkin: null, metin: s[d.etkin], deger: s[d.etkin] };
    return d;
  }
  if (t === "Escape") {
    if (d.acik) return { ...d, acik: false, etkin: null };          // listeyi kapat
    return { ...d, metin: "", deger: null };                        // alani temizle
  }
  if (t === "Tab") return { ...d, acik: false, etkin: null, odakAlanda: false };
  if (t.startsWith("yaz:")) return { ...d, metin: d.metin + t.slice(4), acik: true, etkin: null };
  return d;
}

const uygula = (d, dizi) => dizi.reduce(tus, d);

test("kapaliyken asagi ok listeyi acar ve ilk secenegi etkin yapar", () => {
  const d = uygula(baslangic(), ["ArrowDown"]);
  assert.equal(d.acik, true);
  assert.equal(secenekler(d)[d.etkin], "Mimarlık");
});

test("kapaliyken yukari ok listeyi acar ve son secenegi etkin yapar", () => {
  const d = uygula(baslangic(), ["ArrowUp"]);
  assert.equal(secenekler(d)[d.etkin], "Matematik");
});

test("son secenekte asagi ok basa donmez", () => {
  const d = uygula(baslangic(), ["ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown", "ArrowDown"]);
  assert.equal(d.etkin, 3);
  assert.equal(secenekler(d)[d.etkin], "Matematik");
});

test("giris tusu etkin secenegi deger yapar ve listeyi kapatir", () => {
  const d = uygula(baslangic(), ["ArrowDown", "ArrowDown", "Enter"]);
  assert.equal(d.deger, "Mikrobiyoloji");
  assert.equal(d.acik, false);
  assert.equal(d.etkin, null);
});

test("yazmak listeyi acar ve suzer, etkin secenek sifirlanir", () => {
  const d = uygula(baslangic(), ["yaz:M", "yaz:i"]);
  assert.deepEqual(secenekler(d), ["Mimarlık", "Mikrobiyoloji"]);
  assert.equal(d.etkin, null);
});

test("escape once listeyi kapatir, ikinci basista alani temizler", () => {
  const bir = uygula(baslangic(), ["yaz:M", "yaz:i", "ArrowDown", "Escape"]);
  assert.equal(bir.acik, false);
  assert.equal(bir.metin, "Mi");
  const iki = uygula(bir, ["Escape"]);
  assert.equal(iki.metin, "");
  assert.equal(iki.deger, null);
});

test("sekme tusu listeyi kapatir ve odagi alandan cikarir", () => {
  const d = uygula(baslangic(), ["ArrowDown", "Tab"]);
  assert.equal(d.acik, false);
  assert.equal(d.odakAlanda, false);
});

test("eslesme yokken liste acilir ama etkin secenek olmaz", () => {
  const d = uygula(baslangic(), ["yaz:Z"]);
  assert.deepEqual(secenekler(d), []);
  const e = uygula(d, ["ArrowDown"]);
  assert.equal(e.etkin, null);
});

test("ulasilan butun durumlarda degismezler korunur ve odak alanda kalir", () => {
  const TUSLAR = ["ArrowDown", "ArrowUp", "Enter", "Escape", "yaz:M", "yaz:i", "yaz:a"];
  const anahtar = (d) => [d.acik, d.etkin, d.metin, d.deger].join("|");
  const gorulen = new Map([[anahtar(baslangic()), baslangic()]]);
  const kuyruk = [baslangic()];
  while (kuyruk.length) {
    const d = kuyruk.shift();
    for (const t of TUSLAR) {
      const y = tus(d, t);
      if (y.metin.length > 3) continue;               // tarama uzunlugu sinirli
      if (gorulen.has(anahtar(y))) continue;
      gorulen.set(anahtar(y), y);
      kuyruk.push(y);
    }
  }
  for (const d of gorulen.values()) {
    assert.equal(d.odakAlanda, true, "odak alandan cikti");
    if (!d.acik) assert.equal(d.etkin, null, "kapali listede etkin secenek kaldi");
    if (d.etkin !== null) {
      assert.ok(d.acik, "etkin secenek varken liste kapali");
      assert.ok(d.etkin < secenekler(d).length, "etkin secenek listede yok");
    }
  }
  console.log(`  taranan durum sayisi: ${gorulen.size}`);
});
  taranan durum sayisi: 91
✔ kapaliyken asagi ok listeyi acar ve ilk secenegi etkin yapar (0.406542ms)
✔ kapaliyken yukari ok listeyi acar ve son secenegi etkin yapar (0.0605ms)
✔ son secenekte asagi ok basa donmez (0.065417ms)
✔ giris tusu etkin secenegi deger yapar ve listeyi kapatir (0.327ms)
✔ yazmak listeyi acar ve suzer, etkin secenek sifirlanir (0.311833ms)
✔ escape once listeyi kapatir, ikinci basista alani temizler (0.072417ms)
✔ sekme tusu listeyi kapatir ve odagi alandan cikarir (0.045125ms)
✔ eslesme yokken liste acilir ama etkin secenek olmaz (0.05225ms)
✔ ulasilan butun durumlarda degismezler korunur ve odak alanda kalir (2.111542ms)
ℹ tests 9
ℹ suites 0
ℹ pass 9
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.581875

Süre değerleri koşumdan koşuma değişir. Taranan durum sayısı ise yazma uzunluğunun üç karakterle sınırlanmasına bağlıdır; sınır kaldırıldığında durum uzayı sonsuzdur, çünkü kullanıcı istediği kadar karakter yazabilir.

Değişmezlerin Okunması

Son sınama üç kural denetliyor ve üçü de ekranla ağacın ayrışabildiği yerlerdir.

Kapalı listede etkin seçenek kalamaz. aria-activedescendant değeri temizlenmeden liste kapatılırsa, ağaçta var olmayan bir seçenek etkin görünür. Bu, kullanıcının listeyi kapattıktan sonra hâlâ liste içinde olduğunu sanmasına yol açar.

Etkin seçenek listede bulunmalıdır. Süzme sonrası liste kısaldığında eski dizin geçersizleşir. Sınamada bu, yaz: tuşlarının etkin seçeneği sıfırlamasıyla önleniyor; sıfırlanmadığında bildirim var olmayan bir kimliğe işaret eder ve sessizce düşer.

Odak alandan çıkmaz. Doksan bir durumun tamamında koşul sağlanıyor. Bu, kalıbın tanımı gereğidir: odağın listeye taşındığı bir gerçekleştirim aria-activedescendant kalıbı değil, başka bir kalıptır ve o durumda yazarak süzme olanağı kaybedilir.

Ölçülebilir Kısıtlar

4.1.2 Ad, Rol, Değer. Alanın adı, birleşik giriş alanı rolü, açık/kapalı durumu ve seçili değeri program yoluyla okunabilmelidir. Elle kurulan bileşende dördü de ayrı ayrı yazılır.

2.4.7 Odak Görünürlüğü. Gerçek odak alandadır; göstergesi alanda bulunmalıdır. Etkin seçenek vurgusu bu göstergenin yerine geçmez, ona eklenir.

1.4.11 Metin Dışı Kontrast. Etkin seçenek vurgusu, komşu satırlardan en az 3:1 ile ayrılmalıdır. Yalnızca açık gri bir yüzeyle kurulan vurgu bu ölçüyü karşılamaz.

1.4.13 Üzerine Gelme veya Odaklanmayla Beliren İçerik. Açılan liste odakla beliren bir içeriktir ve üç koşulu karşılamalıdır: Escape ile kapatılabilmeli, imleç listenin üzerine götürülebilmeli ve kullanıcı bir eylem yapmadan kaybolmamalıdır.

Süzme sonucunun duyurulması. Yazarak süzülen bir listede kaç seçeneğin kaldığı, kibar öncelikli bir durum bölgesiyle bildirilir. Bildirim her tuş vuruşunda değil, yazma durduktan sonra gönderilir; aksi durumda duyuru kuyruğu kullanıcının yazdığını duymasını engeller.

Sık Yapılan Hata

Odağın listeye taşınması. Saptaması, liste açıkken yazmayı denemektir: yazılan karakter alana gitmiyorsa odak kaymıştır. Bu kalıpta düzeltme, odağı geri taşımak değil, aria-activedescendant bildirimine geçmektir.

Açık/kapalı durumunun güncellenmemesi. Liste ekranda görünürken aria-expanded değerinin false kalması, ağacın ekranla çelişmesidir. Saptaması bir teklik ve tutarlılık denetimidir: listenin görünürlüğüyle bildirilen durum karşılaştırılır.

Yerel ögenin görünüş uğruna bırakılması. Elle kurulan bileşenin bedeli on bildirimdir ve her biri ayrı bir hata kaynağıdır. Saptaması belirtim aşamasındadır: seçenek satırında ek bilgi, yazarak süzme ya da sunucudan getirme gereksinimlerinden hiçbiri yoksa karar yerel ögedir.

Özet

  • Açılır liste, sayılabilir ama ekrana sığmayan seçenek kümeleri içindir; beşten az seçenek için seçenek grubu, önceden bilinmeyen kümeler için arama kullanılır.
  • select ögesi rolü, adı, durumu, değeri ve klavye sözleşmesini birlikte getirir; bedeli, açılan listenin görünümünün sunum katmanından denetlenememesidir.
  • Elle kurulan bileşen on ayrı bildirimi yeniden üretir; yalnızca satır içi ek bilgi, yazarak süzme ya da sunucudan getirme gerektiğinde seçilir.
  • aria-activedescendant kalıbında odak alandan çıkmaz; listede gezinen bildirimdir ve bu yüzden iki ayrı görsel gösterge gerekir.
  • Ok tuşları uçlarda başa dönmez; dönme davranışı yalnızca tamamı ekranda duran seçenek gruplarına aittir.
  • Kapalı listede etkin seçenek bildirimi kalamaz ve etkin seçenek her zaman süzülmüş listede bulunmalıdır; süzme sonrası bildirimin sıfırlanması bunu sağlar.

Sonraki Adım

Buraya kadarki dört bileşen tek başına ele alındı: bir düğme, bir alan, bir grup, bir liste. Ödünç alma formu ise bunların hepsini bir arada taşır ve yeni sorular çıkarır. Alanlar hangi ölçüte göre gruplanır? Form üç adıma bölündüğünde adımın değiştiği nasıl bildirilir ve odak nereye gider? Gönderim başarısız olduğunda kullanıcı yazdıklarını kaybeder mi? Sonraki ders formu bir bütün olarak belirtir ve tek tek doğru olan bileşenlerin bir arada yanlış çalışabildiği yerleri gösterir.

İ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