Ders 18 / 22
Etkileşim Durumları
Beş etkileşim durumunun tanımı, durum değişikliğinin ölçülebilir eşiğe bağlanması, üzerine gelme ile seçili olmanın kanal çakışması ve odak göstergesinin alan ve kontrast hesabı.
İçindekiler
Önceki konu katalog arayüzünün durağan görüntüsünü tamamladı: metin ölçekten bir basamak, renk rol tablosundan bir değer alıyor ve iki temada da denetlenmiş durumda. Ancak arayüz durağan değil. “Ödünç al” düğmesi imleç üzerine geldiğinde, klavyeyle odaklanıldığında, basılı tutulduğunda ve kayıt rafta olmadığı için kullanılamaz olduğunda farklı görünür.
Bu görünümlerin her biri ayrı bir tasarım kararıdır ve hiçbiri tanımlanmadı. Tanımlanmadıklarında kararlar bileşen yazılırken tek tek verilir; aynı arayüzdeki iki düğme farklı biçimde tepki verir ve hangi tepkinin ne anlama geldiği öğrenilemez.
Beş Durum
Bir etkileşimli bileşenin taşıdığı durumlar şunlardır:
- Varsayılan (default). Bileşenin hiçbir etkileşim altında olmadığı görünümü. Diğer bütün durumlar buna göre tanımlanır.
- Üzerine gelme (hover). İmlecin bileşenin üzerinde olduğu durum. Bildirdiği bilgi şudur: burası tıklanabilir bir yer. Yalnızca imleçli girdilerde vardır.
- Odak (focus). Bileşenin klavye girdisini alacak öge olduğu durum. Bildirdiği bilgi şudur: tuşa basılırsa etkilenecek olan burasıdır.
- Basılı (active). Bileşenin o anda basılı tutulduğu durum. Bildirdiği bilgi, girdinin alındığıdır.
- Devre dışı (disabled). Bileşenin şu anda kullanılamayacağı durum.
Beş durumun ikisi sık karıştırılır. Üzerine gelme ile odak farklı girdi türlerinden gelir ve farklı şeyler bildirir; birini diğerinin yerine kullanmak, klavyeyle gezen kullanıcının arayüzde nerede olduğunu göremediği anlamına gelir. Ekran Boyutu ve Girdi Türü dersinde gösterildiği gibi dokunmatik girdide üzerine gelme durumu hiç oluşmaz; odak ise her girdi türünde oluşur. Bu yüzden üzerine gelme durumuna yalnızca ek bilgi yüklenebilir, taşıyıcı bilgi yüklenemez.
Durumlar birleşebilir. Bir düğme aynı anda hem odaklı hem basılı olabilir; hem odaklı hem üzerine gelinmiş olabilir. Bu yüzden durumlar birbirinin yerine geçen bir liste değil, üst üste binen katmanlardır ve her biri ayrı bir kanal kullanmak zorundadır.
Durum Değişikliği Ölçülebilir Olmalı
Bir durum, varsayılandan ayırt edilemiyorsa yoktur. Ayrımın ölçüsü, önceki derslerde kullanılan kontrast oranıdır; eşik ise bu kursun benimsediği 1.2 değeridir.
// durumlar.mjs — etkilesim durumlarinin varsayilandan ayrilabilirligi ve metin kontrasti function hslRgb(h, s, l) { s /= 100; l /= 100; const k = (n) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return [f(0), f(8), f(4)].map((v) => Math.round(v * 255)); } const onaltilik = (rgb) => "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join(""); function kanal(v) { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); } const parlaklik = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b); function kontrast(a, b) { const [x, y] = [parlaklik(a), parlaklik(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); } const ACIKLIK = { "000": 100, "050": 97, 100: 92, 200: 84, 300: 74, 400: 62, 500: 50, 600: 40, 700: 31, 800: 22, 900: 14 }; const TON = { notr: [214, 8], ana: [214, 62] }; const renk = (aile, b) => hslRgb(TON[aile][0], TON[aile][1], ACIKLIK[b]); const AYRIM_ESIGI = 1.2; // bu kursun benimsedigi en kucuk durum degisikligi orani const METIN_ESIGI = 4.5; const DOLGU_DUGME = [ { durum: "varsayilan", yuzey: ["ana", "600"], metin: ["notr", "000"] }, { durum: "uzerine gelme", yuzey: ["ana", "700"], metin: ["notr", "000"] }, { durum: "basili", yuzey: ["ana", "800"], metin: ["notr", "000"] }, { durum: "devre disi", yuzey: ["notr", "200"], metin: ["notr", "400"] }, ]; const DUZ_DUGME = [ { durum: "varsayilan", yuzey: ["notr", "000"], metin: ["ana", "600"] }, { durum: "uzerine gelme", yuzey: ["ana", "100"], metin: ["ana", "700"] }, { durum: "basili", yuzey: ["ana", "200"], metin: ["ana", "800"] }, { durum: "devre disi", yuzey: ["notr", "000"], metin: ["notr", "400"] }, ]; function denetle(baslik, dizi) { console.log(`\n--- ${baslik} ---`); console.log("durum yuzey metin metin/yuzey yuzey ayrimi metin ayrimi tasiyan kanal"); const t = dizi[0]; for (const d of dizi) { const y = renk(...d.yuzey); const m = renk(...d.metin); const km = kontrast(m, y); const ay = kontrast(y, renk(...t.yuzey)); const am = kontrast(m, renk(...t.metin)); const kanallar = []; if (ay >= AYRIM_ESIGI) kanallar.push("yuzey"); if (am >= AYRIM_ESIGI) kanallar.push("metin"); console.log( `${d.durum.padEnd(15)} ${onaltilik(y)} ${onaltilik(m)} ${km.toFixed(2).padStart(9)}:1 ` + `${ay.toFixed(3).padStart(12)} ${am.toFixed(3).padStart(12)} ` + (d === t ? "-" : kanallar.length ? kanallar.join("+") : "AYRILMIYOR") + (km < METIN_ESIGI ? " [metin esigi alti]" : "") ); } } denetle("birincil dugme (dolgulu)", DOLGU_DUGME); denetle("ikincil dugme (dolgusuz)", DUZ_DUGME); // Sonuc listesi satiri: uzerine gelme ile secili olma ayni kanali paylasiyor console.log("\n--- sonuc listesi satiri ---"); console.log("durum yuzey varsayilana karsi uzerine gelmeye karsi"); const SATIR = [ ["varsayilan", ["notr", "000"]], ["uzerine gelme", ["notr", "050"]], ["secili", ["ana", "100"]], ]; for (const [ad, bas] of SATIR) { const y = renk(...bas); console.log( `${ad.padEnd(15)} ${onaltilik(y)} ${kontrast(y, renk("notr", "000")).toFixed(3).padStart(17)} ${kontrast(y, renk("notr", "050")).toFixed(3).padStart(21)}` ); } // Satir icin uzerine gelme yuzeyi hangi basamakta esigi gecer? console.log("\nuzerine gelme adayi varsayilana karsi secili duruma karsi"); for (const b of ["050", "100", "200"]) { const y = renk("notr", b); console.log( `${("notr-" + b).padEnd(20)} ${kontrast(y, renk("notr", "000")).toFixed(3).padStart(17)} ${kontrast(y, renk("ana", "100")).toFixed(3).padStart(20)}` ); }
--- birincil dugme (dolgulu) --- durum yuzey metin metin/yuzey yuzey ayrimi metin ayrimi tasiyan kanal varsayilan #275ea5 #ffffff 6.50:1 1.000 1.000 - uzerine gelme #1e4980 #ffffff 9.05:1 1.393 1.000 yuzey basili #15335b #ffffff 12.68:1 1.950 1.000 yuzey devre disi #d3d6d9 #969da6 1.88:1 4.454 2.738 yuzey+metin [metin esigi alti] --- ikincil dugme (dolgusuz) --- durum yuzey metin metin/yuzey yuzey ayrimi metin ayrimi tasiyan kanal varsayilan #ffffff #275ea5 6.50:1 1.000 1.000 - uzerine gelme #dee9f7 #1e4980 7.37:1 1.228 1.393 yuzey+metin basili #bdd3ef #15335b 8.29:1 1.530 1.950 yuzey+metin devre disi #ffffff #969da6 2.74:1 1.000 2.374 metin [metin esigi alti] --- sonuc listesi satiri --- durum yuzey varsayilana karsi uzerine gelmeye karsi varsayilan #ffffff 1.000 1.071 uzerine gelme #f7f7f8 1.071 1.000 secili #dee9f7 1.228 1.147 uzerine gelme adayi varsayilana karsi secili duruma karsi notr-050 1.071 1.147 notr-100 1.204 1.020 notr-200 1.459 1.189
Dolgulu düğmede üzerine gelme ve basılı durumları eşiği geçiyor (1.393 ve 1.950) ve ikisi de tek kanalda, yüzeyde duruyor. Bu kabul edilebilir, çünkü ikisi de geçici durumlardır ve bilgi taşımazlar.
Metin kontrastı her durumda korunuyor ve hatta yükseliyor: 6.50, 9.05, 12.68. Bu, yön seçiminin sonucudur; dolgu koyulaştıkça beyaz metnin kontrastı artar. Ters yön seçilseydi basılı durumda metin eşiği kaçırabilirdi.
Düz düğmede iki durum da iki kanalda birden ayrılıyor. Nedeni, dolgusuz düğmede yüzey ayrımının zayıf kalmasıdır: 1.228, eşiğin hemen üstünde. Metin kanalının eşlik etmesi bu zayıflığı karşılıyor.
Devre dışı durumu her iki düğmede de metin eşiğinin altında kalıyor. Karşıtlık ve Erişilebilirlik dersinde görüldüğü gibi etkin olmayan bileşenler 1.4.3 kapsamı dışındadır, yani bu bir ihlal değil. Ancak orada verilen karar burada da geçerlidir: kullanıcının düğmenin ne yaptığını okuması gerekiyorsa, düğme devre dışı bırakılmaz.
Üzerine Gelme ile Seçili Olma Aynı Kanalı Paylaşamaz
Üçüncü tablo, sonuç listesi satırında iki ayrı sorunu birden gösteriyor.
Birincisi, üzerine gelme yüzeyi nötr 050’de duruyor ve varsayılana karşı yalnızca 1.071 kontrast üretiyor. Eşik 1.2, dolayısıyla bu durum ölçülebilir bir değişiklik üretmiyor.
İkincisi ve daha ağırı, seçili durumun üzerine gelme durumuna karşı 1.147 kontrast taşıması. İki durum birbirinden ayrılmıyor: kullanıcı bir satırın seçili mi yoksa imlecin altında mı olduğunu ayırt edemiyor.
Dördüncü tablo, sorunun yüzey kanalı içinde çözülemeyeceğini gösteriyor. Nötr 100 varsayılana karşı eşiği geçiyor (1.204) ama seçili duruma karşı 1.020’ye düşüyor — iki durum tümüyle birleşiyor. Nötr 200 varsayılana karşı rahat geçiyor (1.459) ama seçili duruma karşı 1.189 ile hâlâ eşiğin altında. Üç adayın hiçbiri iki koşulu birden sağlamıyor.
Bunun nedeni yapısaldır: iki durum da aynı eksende, yüzey açıklığında kodlanıyor ve iki değer bir tarafta varsayılandan, diğer tarafta birbirinden ayrılmak zorunda. Eksen bu kadar yükü taşımıyor.
Çözüm, iki durumu ayrı kanallara dağıtmaktır. Seçili olma kalıcı ve bilgi taşıyan bir durumdur; satırın başına konan ve yüzeye karşı 3.0 eşiğini geçen dikey bir çubukla işaretlenir. Üzerine gelme geçicidir; yüzey kanalında kalır ve nötr 100 basamağına taşınarak eşiği geçer. İki durum farklı eksenlerde olduğu için aynı anda oluştuklarında da ayırt edilebilirler. Karşıtlık ve Erişilebilirlik dersindeki seçili satır bulgusu aynı ikinci kanalı istiyordu.
Odak Göstergesi Kaldırılamaz
Odak durumu diğer dördünden ayrılır, çünkü tek başına bir uygunluk koşuluna bağlıdır. 2.4.7 Odak Görünürlüğü ölçütü, klavyeyle kullanılabilen her ögenin görünür bir odak göstergesi taşımasını ister. Gösterge kaldırılamaz; yalnızca değiştirilebilir.
Göstergenin nasıl olacağı iki ölçüye bağlıdır. Kontrast ölçüsü 1.4.11’den gelir: gösterge komşu renklerinden en az 3:1 ile ayrılmalıdır. Alan ölçüsü 2.4.13 Odak Görünümü ölçütünden gelir: göstergenin kapladığı alan, bileşenin 2 piksel kalınlığındaki çevresinin alanı kadar olmalıdır.
// odak.mjs — odak gostergesinin alan ve kontrast olcutlerine gore denetimi function hslRgb(h, s, l) { s /= 100; l /= 100; const k = (n) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return [f(0), f(8), f(4)].map((v) => Math.round(v * 255)); } const onaltilik = (rgb) => "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join(""); function kanal(v) { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); } const parlaklik = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b); function kontrast(a, b) { const [x, y] = [parlaklik(a), parlaklik(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); } // Alan olcutu: gosterge alani, bilesenin 2 px kalinliginda cevresi kadar olmali. const gereken = (w, h) => (w + 4) * (h + 4) - w * h; // Halka alani: kalinlik k, bosluk (offset) o function halkaAlani(w, h, k, o) { const disW = w + 2 * (o + k), disH = h + 2 * (o + k); const icW = w + 2 * o, icH = h + 2 * o; return disW * disH - icW * icH; } const BILESENLER = [ { ad: "odunc al dugmesi", w: 132, h: 44 }, { ad: "simge dugmesi", w: 24, h: 24 }, ]; const ADAYLAR = [ { ad: "1px, bosluksuz", k: 1, o: 0 }, { ad: "2px, bosluksuz", k: 2, o: 0 }, { ad: "2px, 2px bosluk", k: 2, o: 2 }, { ad: "3px, 1px bosluk", k: 3, o: 1 }, ]; for (const b of BILESENLER) { const g = gereken(b.w, b.h); console.log(`\n--- ${b.ad} (${b.w}x${b.h} px) --- gereken en kucuk alan: ${g} px2`); console.log("gosterge alan (px2) gereken alana orani sonuc"); for (const a of ADAYLAR) { const alan = halkaAlani(b.w, b.h, a.k, a.o); console.log( `${a.ad.padEnd(17)} ${String(alan).padStart(10)} ${(alan / g).toFixed(3).padStart(20)} ${alan >= g ? "gecti" : "KALDI"}` ); } } // Kontrast olcutu: halka hem sayfa yuzeyinden hem bilesenin dolgusundan ayrilmali. const ACIKLIK = { "000": 100, 100: 92, 500: 50, 600: 40, 700: 31, 900: 14 }; const TON = { notr: [214, 8], ana: [214, 62] }; const renk = (aile, b) => hslRgb(TON[aile][0], TON[aile][1], ACIKLIK[b]); const YUZEY = renk("notr", "000"); const DOLGU = renk("ana", "600"); console.log("\nhalka rengi yuzeye karsi dolguya karsi ikisi de >=3.0"); for (const [aile, bas] of [["ana", "500"], ["ana", "700"], ["notr", "900"], ["notr", "500"]]) { const h = renk(aile, bas); const ky = kontrast(h, YUZEY); const kd = kontrast(h, DOLGU); console.log( `${(aile + "-" + bas).padEnd(13)} ${ky.toFixed(2).padStart(12)} ${kd.toFixed(2).padStart(13)} ${ky >= 3 && kd >= 3 ? "gecti" : "KALDI"}` ); } // Iki katmanli halka: ic katman yuzey renginde, dis katman koyu. console.log("\niki katmanli halka (ic: yuzey, dis: notr-900)"); const ic = YUZEY, dis = renk("notr", "900"); console.log(`ic katman / dolgu : ${kontrast(ic, DOLGU).toFixed(2)}:1`); console.log(`dis katman / yuzey: ${kontrast(dis, YUZEY).toFixed(2)}:1`); console.log(`dis katman / ic : ${kontrast(dis, ic).toFixed(2)}:1`); console.log(`alan (132x44, ic 2px + dis 2px): ${halkaAlani(132, 44, 4, 0)} px2, gereken ${gereken(132, 44)} px2`);
--- odunc al dugmesi (132x44 px) --- gereken en kucuk alan: 720 px2 gosterge alan (px2) gereken alana orani sonuc 1px, bosluksuz 356 0.494 KALDI 2px, bosluksuz 720 1.000 gecti 2px, 2px bosluk 752 1.044 gecti 3px, 1px bosluk 1116 1.550 gecti --- simge dugmesi (24x24 px) --- gereken en kucuk alan: 208 px2 gosterge alan (px2) gereken alana orani sonuc 1px, bosluksuz 100 0.481 KALDI 2px, bosluksuz 208 1.000 gecti 2px, 2px bosluk 240 1.154 gecti 3px, 1px bosluk 348 1.673 gecti halka rengi yuzeye karsi dolguya karsi ikisi de >=3.0 ana-500 4.59 1.41 KALDI ana-700 9.05 1.39 KALDI notr-900 15.74 2.42 KALDI notr-500 4.11 1.58 KALDI iki katmanli halka (ic: yuzey, dis: notr-900) ic katman / dolgu : 6.50:1 dis katman / yuzey: 15.74:1 dis katman / ic : 15.74:1 alan (132x44, ic 2px + dis 2px): 1472 px2, gereken 720 px2
Alan tablosu tek satırlık bir sonuç veriyor: bir piksellik halka ölçütün yarısını karşılıyor. Oran her iki bileşende de yaklaşık aynı — 0.494 ve 0.481 — çünkü ölçüt zaten bileşenin çevresine oranlıdır. İki piksellik halka ölçütü tam olarak karşılıyor; boşluk eklendiğinde alan biraz daha büyüyor.
Kontrast tablosu daha sert bir sonuç veriyor. Dört halka rengi adayının hiçbiri iki koşulu birden sağlamıyor. Nedeni, halkanın iki farklı komşusu olmasıdır: dışarıda açık sayfa yüzeyi, içeride koyu düğme dolgusu. Yüzeyden ayrılmak için koyu olmak gerekiyor, dolgudan ayrılmak için açık — tek bir renk ikisini birden yapamıyor.
Son blok çözümü veriyor. Halka iki katmanlı çizilir: içteki katman sayfa yüzeyi renginde, dıştaki katman koyu. İç katman düğmenin dolgusuyla 6.50, dış katman sayfa yüzeyiyle 15.74 kontrast taşıyor; iki koşul da sağlanıyor. Toplam kalınlık 4 piksel olduğunda alan 1472 piksel kare, gereken 720’nin iki katından fazla.
Bu çözümün bir yan sonucu var: zemin ne olursa olsun katmanlardan biri mutlaka komşusundan ayrılır. Odak göstergesini bileşen bileşen ayarlamak yerine tek kural olarak yazmayı bu özellik mümkün kılar.
Durum Tablosu Bileşenin Sözleşmesidir
Bir bileşenin tasarımı, tek bir görünümle değil durum tablosuyla tamamlanır. Tablonun her satırı bir durum, her sütunu bir kanaldır; boş bırakılan hücre, o durumun o kanalda değişmediğini söyler.
Tablonun eksiksiz olması, tasarımın eksiksiz olması demektir. En sık atlanan satırlar odak ve devre dışıdır; ikisi de bir etkileşim gerektirdiği için tasarım gözden geçirilirken ekranda görünmez. Tablonun yazılı olması bu atlanmayı önler.
Tablodan çıkan ikinci sonuç, durumların bileşene değil role tanımlanmasıdır. Aynı durum tanımı arama düğmesinde, filtre uygulama düğmesinde ve iade uzatma düğmesinde de geçerlidir; rol düzeyinde tanımlandığında yeni bir düğme için karar yeniden verilmez.
Özet
- Etkileşimli bileşenlerin beş durumu vardır ve her biri farklı bir bilgi bildirir; üzerine gelme yalnızca imleçli girdide oluştuğu için taşıyıcı bilgi yüklenemez.
- Durum değişikliği ölçülebilir olmalıdır; varsayılandan ayrım bir eşiğe bağlanır ve hangi kanalda gerçekleştiği yazılır.
- Dolgu koyulaştırılarak kurulan durum geçişleri metin kontrastını yükseltir; ters yön seçildiğinde metin eşiği kaçırabilir.
- Üzerine gelme ile seçili olma aynı eksende kodlanamaz; yüzey açıklığı iki ayrımı birden taşıyamadığı için kalıcı durum ikinci bir kanala taşınır.
- Odak göstergesi kaldırılamaz; alan ölçütü bileşenin iki piksellik çevresi kadar alan, kontrast ölçütü ise komşularından 3:1 ayrım ister.
- Tek renkli bir halka iki farklı komşuyu birden karşılayamaz; iki katmanlı halka her zemin üzerinde çalışır ve tek kural olarak yazılabilir.
Sonraki Adım
Beş durumun tamamı bileşenin hazır olduğu varsayımına dayanıyordu. Ancak katalog arayüzü verinin gelmesini bekliyor olabilir, arama hiçbir sonuç döndürmemiş olabilir ya da kullanıcı henüz hiçbir şey aramamış olabilir. Bu üç durumda ekranda gösterilecek bir bileşen yoktur ve tasarlanacak şey beklemenin ve boşluğun kendisidir. Sonraki ders yükleme göstergesinin hangi süre eşiklerinde değiştiğini, iskelet gösteriminin ne zaman yanıltıcı olduğunu ve boş durumun neden bir hata iletisi olmadığını ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.