İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat