Ders 10 / 25
Menüler ve Açılır Paneller
Menü rolüyle açılır panelin ayrımı, üç kurulumun odak kaçağı ve kapanma davranışı bakımından taranması, odak dönüş noktasının ve aç-kapa döngüsünün doğrulanması.
İçindekiler
Sekme ve akordeon kalıplarında açılan içerik sayfanın akışı içinde kaldı: bir yer kapladı, altındaki içeriği aşağı itti ve odak kendi başına ilerledi. Katalogun üst çubuğundaki “Hesabım” denetimi farklı davranır — açtığı liste sayfanın üstünde durur, altındaki içeriği örter ve dışına tıklandığında kapanır.
Bu, kapanmanın bir davranış olarak belirtilmesi gereken ilk kalıptır. Kapanma birden çok yolla olur ve her yolun ardından odağın nereye gideceği ayrı bir karardır. Ders iki kalıbı birlikte ele alır, çünkü ekranda birbirine benzemelerine karşın sözleşmeleri ayrıdır.
İki Kalıp, İki Sözleşme
Açılır panel, bir düğmenin gösterip gizlediği sıradan içeriktir. İçinde bağlantı,
düğme, alan, hatta bir form bulunabilir. Akordeonla aynı bildirimi kullanır —
aria-expanded — ve tek farkı içeriğin sayfanın üstünde durmasıdır.
Menü, uygulama benzeri komut listeleridir ve kendi rol kümesiyle kurulur: role="menu",
role="menuitem", role="menuitemcheckbox". Bu rollerin bir sözleşmesi vardır ve
sözleşme kısıtlayıcıdır: menü içinde yalnız menü ögeleri bulunur, gezinme ok tuşlarıyla
yapılır ve menünün tamamı tek bir sekme durağıdır.
Karar ölçütü içeriktir. “Hesabım” denetiminin açtığı liste — “Ödünçlerim”, “Ayarlar”, “Çıkış” — sıradan bağlantılardan oluşur ve bir gezinme listesidir; kalıbı açılır paneldir. Menü rolü, sonuç tablosundaki “Dışa aktar” düğmesinin açtığı komut listesi gibi, ögeleri gezinme değil eylem olan listelerde kullanılır.
Yanlış seçim sessiz bir maliyet üretir. Bağlantılardan oluşan bir listeye menü rolü yazıldığında bağlantılar ağaçta menü ögesi olarak bildirilir; kullanıcının bağlantı listesinden onlara ulaşması olanaksızlaşır ve “yeni sekmede aç” beklentisi karşılanmaz.
Yerel Öge Önce
İkisinin de yerel karşılığı yoktur. Açan denetim button ögesidir; açılır panelde
aria-expanded ve aria-controls yazılır, menüde ayrıca aria-haspopup="menu" bildirilir.
<button id="hesabim" aria-expanded="false" aria-controls="hesap-paneli">Hesabım</button> <div id="hesap-paneli" hidden> <ul> <li><a href="/hesap/oduncler">Ödünçlerim</a></li> <li><a href="/hesap/ayarlar">Ayarlar</a></li> </ul> <form action="/cikis" method="post"><button type="submit">Çıkış</button></form> </div>
Panelin içeriği sıradan işaretlemedir; liste liste, form form olarak kalır. Bu, kalıbın menüye yeğlenmesinin gerekçesidir: hiçbir yapı anlamı elle yeniden üretilmez.
Kapanma Yolları ve Odak Dönüşü
Kapanma dört yolla olur: açan düğmenin yeniden çalıştırılması, Escape tuşu, panelin
dışına tıklanması ve panel içinde bir ögenin seçilmesi.
Odağın nereye gideceği kapanma yoluna göre değişir ve kural şudur: kullanıcının başlattığı
kapanışta odak açan düğmeye döner. Escape tuşuyla ve düğmenin yeniden çalıştırılmasıyla
kapanışta bu geçerlidir. Panel içindeki bir bağlantı seçildiğinde ise gezinme olur ve odak
yeni sayfaya gider; dönüş gereksizdir.
Dışarı tıklama özel bir durumdur. Tıklayan kullanıcı odağı zaten tıkladığı yere taşımıştır; odağı açan düğmeye döndürmek onu tıkladığı yerden geri çeker. Kural: dışarı tıklamada odak yalnız hiçbir yere yerleşmediyse açan düğmeye taşınır.
// menu.mjs — acilir panelde odak kusatmasi, kapanma ve odak donus noktasinin denetimi const ARKA = ["a#katalog", "button#hesabim", "input#ara"]; const ICERIK = ["a#odunclerim", "a#ayarlar", "button#cikis"]; const TETIK = "button#hesabim"; const TUSLAR = ["Tab", "Shift+Tab", "ArrowDown", "ArrowUp", "Escape", "Enter", "DisariTik"]; // tur: "panel" (acilir panel) | "menu" (menu rolu) | "kusurlu" function makine(tur) { const arkaGit = (i, yon) => ARKA[(i + yon + ARKA.length) % ARKA.length]; return (d, t) => { if (!d.acik) { const i = ARKA.indexOf(d.odak); if (t === "Tab") return { acik: false, odak: arkaGit(i, 1) }; if (t === "Shift+Tab") return { acik: false, odak: arkaGit(i, -1) }; if ((t === "Enter" || t === "ArrowDown") && d.odak === TETIK) return { acik: true, odak: ICERIK[0] }; return d; } const j = ICERIK.indexOf(d.odak); if (tur === "menu") { if (t === "ArrowDown") return { acik: true, odak: ICERIK[(j + 1) % ICERIK.length] }; if (t === "ArrowUp") return { acik: true, odak: ICERIK[(j + ICERIK.length - 1) % ICERIK.length] }; if (t === "Tab") return { acik: false, odak: arkaGit(ARKA.indexOf(TETIK), 1) }; if (t === "Shift+Tab") return { acik: false, odak: TETIK }; if (t === "Escape") return { acik: false, odak: TETIK }; if (t === "DisariTik") return { acik: false, odak: TETIK }; return d; } if (tur === "panel") { if (t === "Tab") return j === ICERIK.length - 1 ? { acik: false, odak: arkaGit(ARKA.indexOf(TETIK), 1) } : { acik: true, odak: ICERIK[j + 1] }; if (t === "Shift+Tab") return j === 0 ? { acik: true, odak: TETIK } : { acik: true, odak: ICERIK[j - 1] }; if (t === "Escape") return { acik: false, odak: TETIK }; if (t === "DisariTik") return { acik: false, odak: TETIK }; return d; } // kusurlu: escape yok, disari tikta odak kayboluyor, Tab arka plana kaciyor if (t === "Tab") return j === ICERIK.length - 1 ? { acik: true, odak: ARKA[0] } : { acik: true, odak: ICERIK[j + 1] }; if (t === "Shift+Tab") return { acik: true, odak: ICERIK[Math.max(j - 1, 0)] }; if (t === "DisariTik") return { acik: false, odak: "(belge koku)" }; return d; }; } const anahtar = (d) => (d.acik ? "acik" : "kapali") + "|" + d.odak; function denetle(tur) { const sonraki = makine(tur); const bas = { acik: false, odak: TETIK }; const gorulen = new Map([[anahtar(bas), bas]]); const kuyruk = [bas]; const kapanislar = []; // [oncekiDurum, tus, sonrakiDurum] while (kuyruk.length) { const d = kuyruk.shift(); for (const t of TUSLAR) { const y = sonraki(d, t); if (d.acik && !y.acik) kapanislar.push([d, t, y]); if (gorulen.has(anahtar(y))) continue; gorulen.set(anahtar(y), y); kuyruk.push(y); } } const durumlar = [...gorulen.values()]; const escapeIle = kapanislar.filter(([, t]) => t === "Escape"); return { durum: durumlar.length, kacak: durumlar.some((d) => d.acik && ARKA.includes(d.odak) && d.odak !== TETIK), escapeCikisi: escapeIle.length > 0, escapeDonusu: escapeIle.length > 0 && escapeIle.every(([, , y]) => y.odak === TETIK), odakKaybi: kapanislar.some(([, , y]) => !ARKA.includes(y.odak) && y.odak !== TETIK), dongu: anahtar(sonraki(sonraki(bas, "Enter"), "Escape")) === anahtar(bas), icDurak: tur === "menu" ? 1 : ICERIK.length, }; } const im = (b) => (b ? "var" : "yok"); console.log("kurulum".padEnd(10) + "durum".padEnd(8) + "ic durak".padEnd(10) + "arka plana kacak".padEnd(18) + "Escape cikisi".padEnd(15) + "tetige donus".padEnd(14) + "odak kaybi".padEnd(12) + "ac-kapa dongusu"); for (const tur of ["panel", "menu", "kusurlu"]) { const s = denetle(tur); console.log(tur.padEnd(10) + String(s.durum).padStart(5).padEnd(8) + String(s.icDurak).padStart(4).padEnd(10) + im(s.kacak).padEnd(18) + im(s.escapeCikisi).padEnd(15) + im(s.escapeDonusu).padEnd(14) + im(s.odakKaybi).padEnd(12) + (s.dongu ? "kapali" : "ACIK")); } // Dogru kurulumda odak izi console.log("\nacilir panel kurulumunda odak izi:"); let d = { acik: false, odak: TETIK }; const sonraki = makine("panel"); console.log(" " + "baslangic".padEnd(12) + "kapali".padEnd(9) + "odak: " + d.odak); for (const t of ["Enter", "Tab", "Tab", "Escape", "Tab"]) { d = sonraki(d, t); console.log(" " + t.padEnd(12) + (d.acik ? "acik" : "kapali").padEnd(9) + "odak: " + d.odak); }
kurulum durum ic durak arka plana kacak Escape cikisi tetige donus odak kaybi ac-kapa dongusu panel 8 3 yok var var yok kapali menu 6 1 yok var var yok kapali kusurlu 8 3 var yok yok var ACIK acilir panel kurulumunda odak izi: baslangic kapali odak: button#hesabim Enter acik odak: a#odunclerim Tab acik odak: a#ayarlar Tab acik odak: button#cikis Escape kapali odak: button#hesabim Tab kapali odak: input#ara
Bulguların Okunması
İç durak sütunu iki kalıbı ayırıyor. Açılır panelde içerik doğal sekme sırasındadır ve üç durak tutar; menüde dönen sekme durağı vardır ve tamamı tek durak sayılır. Bu, işaretlemeye bakmadan hangi kalıbın kurulduğunu söyleyen ölçüdür.
Aç-kapa döngüsü kapalı olmalıdır. Son sütun, düğmeye odaklanmış bir kullanıcının paneli
açıp Escape ile kapattığında birebir aynı duruma dönüp dönmediğini sınıyor. Doğru iki
kurulumda döngü kapalı; kusurlu kurulumda açık, çünkü Escape tuşu hiçbir şey yapmıyor.
Döngünün kapalı olması, kullanıcının paneli yanlışlıkla açtığında kaybettiği hiçbir şey
olmadığı anlamına gelir.
Kusurlu kurulum üç kusuru birden taşıyor. Panel açıkken Tab tuşu arka plandaki
denetimlere kaçıyor: kullanıcı görmediği, panelin altında kalan ögelere odaklanır.
Escape tuşuyla çıkış yolu yok — bu, 2.1.2 ölçütünün konusudur, ancak burada tam bir
tuzak değildir, çünkü Tab ile kaçış vardır. Dışarı tıklamada odak belgenin köküne
düşüyor ve kullanıcı dolaşıma en baştan başlıyor.
Menü altı, panel sekiz duruma ulaşıyor. Fark, panelin açıkken açan düğmeye geri
dönebilmesinden gelir: Shift+Tab tuşuyla ilk ögeden düğmeye çıkılır ve panel açık kalır.
Menüde aynı tuş menüyü kapatır. İkisi de tanımlıdır; belirtimde hangisinin seçildiği
yazılır.
Klavye Sözleşmesi
Açılır panel:
| Tuş | Davranış |
|---|---|
Enter / Boşluk (düğmede) |
Paneli açar; odak ilk ögeye taşınır. |
Aşağı ok (düğmede) |
Paneli açar ve odağı ilk ögeye taşır. |
Tab |
Panel ögeleri arasında ilerler; son ögeden sonra paneli kapatır. |
Escape |
Paneli kapatır; odak açan düğmeye döner. |
Menü:
| Tuş | Davranış |
|---|---|
Enter / Boşluk / Aşağı ok (düğmede) |
Menüyü açar; odak ilk ögeye taşınır. |
Aşağı ok / Yukarı ok |
Ögeler arasında gezer; uçlarda başa döner. |
Home / End |
İlk ve son ögeye gider. |
Escape |
Menüyü kapatır; odak açan düğmeye döner. |
Tab |
Menüyü kapatır ve odağı sonraki sayfa durağına taşır. |
Menü kalıbında Tab tuşunun menüyü kapatması bir kısıt değil, tanımın parçasıdır: menü
tek durak olduğu için içinde ilerleyecek bir sekme sırası yoktur.
Ölçülebilir Kısıtlar
2.1.2 Klavye Tuzağı Yok. Panel açıkken klavyeyle çıkılabilmelidir. Escape tuşu bu
çıkışın en kısa yoludur ve her iki kalıpta da zorunlu sayılır.
1.4.13 Üzerine Gelme veya Odaklanmayla Beliren İçerik. Panel imleçle açılıyorsa üç
koşul gerekir: Escape ile kapatılabilmeli, imleç panelin üzerine götürülebilmeli ve
kullanıcı bir eylem yapmadan kaybolmamalıdır. Üzerine gelindiğinde açılıp imleç kaydığında
kapanan menüler bu ölçütü kaldırır; ayrıca dokunmatik girdide üzerine gelme durumu hiç
oluşmadığı için kalıp tıklamayla da açılabilmelidir.
2.4.7 Odak Görünürlüğü. Panel içindeki ögelerin odak göstergesi, panelin kendi yüzeyi üzerinde de yeterli karşıtlık taşımalıdır; sayfa yüzeyine göre ayarlanmış tek renkli bir halka panel yüzeyinde kaybolabilir.
1.4.11 Metin Dışı Kontrast. Panelin sınırı, altındaki içerikle karışmayacak biçimde 3:1 ölçüsünü karşılamalıdır; panel yalnız gölgeyle ayrılıyorsa sınır görünmez.
2.5.8 Hedef Boyutu. Panel ögeleri 24 piksel ölçüsüne girer; dar açılır listelerde satır yüksekliği bu ölçüyü belirler.
Sık Yapılan Hata
Bağlantı listesine menü rolü yazılması. Saptaması bir yapı denetimidir: role="menu"
taşıyan ögenin altında menü ögesi olmayan bir çocuk varsa kalıp yanlış kurulmuştur.
Kapanışta odağın bırakılması. Panel kapandığında odak silinen bir ögenin üzerindeyse belgenin köküne düşer. Saptaması, denetimdeki döngü sınamasıdır: aç-kapa dizisi başlangıç durumuna dönmüyorsa odak yönetimi eksiktir.
Escape tuşunun yalnız panele bağlanması. Kullanıcı odağı açan düğmeye geri aldığında
Escape tuşu çalışmıyorsa, kapanmanın bir yolu eksilir. Tuş, düğme ve panelin ortak
atasına bağlanır.
Panelin ağaçtan gizlenmemesi. Sunum katmanında gizlenen bir panel ağaçta kalır ve kullanıcı, kapalı olduğunu sandığı listenin ögelerini okur.
Özet
- Açılır panel sıradan içeriği gösterip gizler; menü, ögeleri komut olan listeler içindir ve kısıtlayıcı bir rol sözleşmesi taşır.
- Bağlantılardan oluşan bir listeye menü rolü yazmak, bağlantıları ağaçta menü ögesine çevirir ve bağlantı listesinden ulaşılmasını engeller.
- Açılır panelin içeriği doğal sekme sırasındadır; menü dönen sekme durağı kullanır ve tek durak tutar. Bu ölçü iki kalıbı ayırt eder.
- Kullanıcının başlattığı her kapanışta odak açan düğmeye döner; panel içinden gezinilerek yapılan kapanışta dönüş gerekmez.
- Aç-kapa döngüsü kapalı olmalıdır: düğmede başlayan bir aç–kapat dizisi birebir aynı duruma dönmelidir.
- Panel açıkken arka plandaki denetimlere odak kaçmamalı ve
Escapeher koşulda bir çıkış yolu bırakmalıdır.
Sonraki Adım
Buraya kadarki kapsayıcılar içeriği gösterip gizledi; hiçbiri kullanıcıya nerede olduğunu söylemedi. Katalogda ise konum bir bilgidir: kullanıcı üç düzey aşağıda bir konu dalındadır ve sonuç listesinin dördüncü sayfasındadır. İki bileşen bu bilgiyi taşır ve ikisi de aynı bildirime dayanır — bulunulan yerin diğerlerinden ayrılması. Sonraki ders kırıntı yolunu ve sayfalamayı belirtir; konumun nasıl bildirileceğini ve sayfa değişiminin neden bir odak kararı gerektirdiğini ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.