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-activedescendantkalı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.