IlmHamroh
JavaScript Full-stack/9-qism. JavaScript brauzerda va debugging26/29-dars23 daqiqa
Mundarija (33)

Dinamik UI'da qulaylik (a11y): aria-live, fokus va aria-expanded JavaScript bilan

Qisqacha: JavaScript sahifani o'zgartirganda ekran o'quvchi buni o'zi sezmaydi — unga uch narsa kerak. Holat atributda yangilansin (aria-expanded, aria-selected), fokusdan tashqaridagi xabar oldindan turgan jonli hududga (role="status") yozilsin, fokus esa hech qachon "yo'qolmasin": o'chirilgan element o'rniga qo'shnisiga, yopilgan oynadan uni ochgan tugmaga qaytsin. Tablar va asboblar paneli strelkalar bilan boshqariladi — tabindex ni JavaScript ko'chiradi (roving tabindex).

Bu darsda

  • aria-expanded va aria-selected ni JavaScript'da holat bilan birga yangilaysiz.
  • Jonli hudud orqali xabarni ekran o'quvchiga aytasiz va qidiruvda ortiqcha e'lonlarni to'xtatasiz.
  • Element o'chganda va oyna yopilganda fokusni to'g'ri joyga qo'yasiz.
  • Modal oynaning fokus tuzog'ini tushunasiz: showModal() va inert.
  • Roving tabindex bilan tablarni strelka, Home va End bilan ishlatasiz.

Oldin bilishingiz kerak: Constraint Validation API, Klaviatura hodisalari va tezkor tugmalar, ARIA asoslari, Jonli hududlar va dinamik kontent, Klaviatura va fokus boshqaruvi.

1. Nega bu kerak?

Malika — «Bahor»ning doimiy mehmoni. U ko'rmaydi va kompyuterda NVDA ekran o'quvchisi bilan ishlaydi: dastur sahifani ovoz chiqarib o'qiydi, Malika esa klaviatura bilan yuradi. HTML qismida biz «Bahor» saytini unga qulay qildik: sarlavhalar, label lar, alt matnlar (Accessibility daraxti).

Keyin saytga JavaScript keldi va uch joyda muammo chiqdi:

  1. Malika "Savatga" tugmasini bosdi. Ekranda "Osh savatga qo'shildi" chiqdi — NVDA esa jim. Malika yana bosdi. Savatda ikki osh.
  2. "Ish vaqti" tugmasini bosdi. Panel ochildi, lekin NVDA tugmani hamon "yig'ilgan" deb o'qiyapti.
  3. Savatdan mantini o'chirdi. Fokus o'chgan tugma bilan birga yo'qoldi. Keyingi Tab Malikani sahifaning boshqa joyiga otib yubordi.

Faqat klaviatura bilan ishlaydigan Bobur ham qiynaldi: menyu tablari orasida strelkalar ishlamaydi, sichqonchasiz tab almashtirib bo'lmaydi.

HTML'da bularning hammasi to'g'ri edi. Muammo — o'zgarishda. Sahifa qotib tursa, brauzer uni ekran o'quvchiga o'zi tushuntiradi. Sahifani JavaScript o'zgartirsa, tushuntirish ham JavaScript'ning zimmasida. ARIA asoslari, Jonli hududlar, ARIA Authoring Practices darslarida "buni JavaScript qiladi" degan va'dalar shu darsda bajariladi.

1.1 Ekran o'quvchi sahifani qanday "ko'radi"

Ekran o'quvchi DOM'ni to'g'ridan-to'g'ri o'qimaydi. Brauzer DOM'dan accessibility daraxti (qulaylik daraxti) quradi: har element uchun rol, nom va holat (Accessibility daraxti). JavaScript DOM'ni o'zgartirsa, daraxt ham yangilanadi. Lekin ekran o'quvchi odatda faqat ikki narsani ovoz chiqarib aytadi: fokusdagi element va jonli hududdagi o'zgarish.

flowchart TD
  JS["JS DOM'ni o'zgartirdi"] --> D["Accessibility daraxti yangilandi"]
  D --> Q{"O'zgarish qayerda?"}
  Q -->|"Fokusdagi elementda"| F["NVDA aytadi: yangi holat"]
  Q -->|"Jonli hududda"| L["NVDA aytadi: yangi matn"]
  Q -->|"Boshqa joyda"| S["Jim. Malika bilmaydi"]

Nimaga qarang: uchinchi yo'l — eng ko'p uchraydigani. Bu darsdagi har bir usul o'zgarishni birinchi ikki yo'ldan biriga o'tkazadi.

1.2 Biz qanday tekshirdik

Bu darsdagi misollarni NVDA'da sinab ko'rish imkonimiz bo'lmadi. Ularni boshqa ishonchli usul bilan tekshirdik: Chrome qurgan accessibility daraxtini kichik dastur yordamida o'qidik. Ekran o'quvchi aynan shu daraxtdan o'qiydi. Daraxtda holat to'g'ri bo'lsa, NVDA, JAWS va VoiceOver uni o'z tilida aytadi.

Siz ham buni qo'lda qilasiz: DevTools → Elements → o'ng paneldagi Accessibility yorlig'i. U tanlangan elementning rolini, nomini va holatini ko'rsatadi.

2. Holat atributda: aria-expanded

2.1 Ochiladigan panel

«Bahor» saytida "Ish vaqti" tugmasi bor: bosilganda ostida jadval ochiladi. Bu tayyor namuna (pattern) disclosure (ochib-yopiladigan blok) deyiladi (ARIA Authoring Practices). Ekran o'quvchiga ikki narsa kerak: tugma panelni boshqarishi (aria-controls) va panel hozir ochiqmi (aria-expanded).

html
<button type="button" id="vaqt-tugma"
        aria-expanded="false" aria-controls="ish-vaqti">
  Ish vaqti
</button>
<div id="ish-vaqti" hidden>
  <p>Har kuni 7:00–23:00. Bron qabul qilinadi: 10:00–22:00.</p>
</div>
<script>
  const tugma = document.querySelector("#vaqt-tugma");
  const panel = document.querySelector("#ish-vaqti");

  tugma.addEventListener("click", () => {
    const ochiq = tugma.getAttribute("aria-expanded") === "true";
    tugma.setAttribute("aria-expanded", String(!ochiq));
    panel.hidden = ochiq;
    console.log("aria-expanded:", tugma.ariaExpanded);
  });
</script>

Tugmani bosing — panel ochiladi, konsolda aria-expanded: true. Yana bosing — yopiladi, aria-expanded: false. Accessibility daraxtida tugma holati ham shunday almashdi: expanded: false → expanded: true → false.

Kodning asosiy g'oyasi — holat bitta joyda. Panel ochiqmi degan savolga aria-expanded javob beradi. JavaScript undan o'qiydi, ko'rinishni o'zgartiradi va atributni yangilaydi. Ekran o'quvchi ham, CSS ham ([aria-expanded="true"] selektori bilan) aynan shu atributga qaraydi — ular hech qachon bir-biriga zid bo'lmaydi. State → render darsidagi g'oyaning qulaylikdagi ko'rinishi.

String(!ochiq) — atribut qiymati doim satr: "true" yoki "false". Atributga mantiqiy true bersangiz ham, u satrga aylanadi, lekin yozuvni aniq qilish yaxshi odat.

Konsol qatoridagi tugma.ariaExpanded — shu atributning JavaScript xususiyati, getAttribute("aria-expanded") ning qisqa yo'li. Qiymati ham satr: "true" yoki "false". Bunday xususiyatlar deyarli har aria-* atribut uchun bor (ariaSelected, ariaPressed). web-features ma'lumotiga ko'ra ular 2023-yil oktabridan barcha asosiy brauzerlarda va 2026-yil aprelidan "keng tarqalgan" Baseline. Ikkala yo'l ham to'g'ri; kursda ko'pincha setAttribute ni ko'rasiz — eski kodda ham, hujjatlarda ham u ko'proq uchraydi.

2.2 Unutilgan holat

ARIA asoslari darsidagi xatoni eslang: aria-expanded="false" yozilgan, menyu ochilganda ham "false" qoldi. Bu JavaScript'da qanday paydo bo'ladi:

html
<button type="button" id="vaqt-tugma"
        aria-expanded="false" aria-controls="ish-vaqti">
  Ish vaqti
</button>
<div id="ish-vaqti" hidden>
  <p>Har kuni 7:00–23:00.</p>
</div>
<script>
  const tugma = document.querySelector("#vaqt-tugma");
  const panel = document.querySelector("#ish-vaqti");

  tugma.addEventListener("click", () => {
    panel.hidden = !panel.hidden;
  });
</script>

Ko'ziga ko'rinadigan hamma narsa ishlaydi: bosiladi, ochiladi, yopiladi. Daraxtda esa tugma doim expanded: false. Malika panel ochilganini bilmaydi va o'qishni to'xtatadi. Bunday xatoni ko'z bilan topib bo'lmaydi — faqat Accessibility yorlig'i yoki ekran o'quvchi bilan. Tuzatish: ko'rinishni o'zgartiradigan har qatorga juft qilib holat atributini yangilang. Yaxshisi — ko'rinishni atributdan chiqaring («Ochiladigan panel» dagi kabi).

2.3 Avval tayyor element

Holatni o'zi yangilaydigan HTML elementlari bor — ularda xato qilishning iloji ham yo'q:

  • details va summary — ochilgan-yopilganini brauzer o'zi aytadi (Tabs va accordion).
  • popovertarget li tugma (Popover API) — Chrome 154 da tekshirdik: popover ochilganda tugma daraxtda expanded: true bo'ladi, hech qanday ARIA'siz. Qiziq fakt: bunday tugmaga qo'lda aria-expanded="false" yozib qo'ysak ham, daraxtda baribir haqiqiy holat chiqdi. Shuning uchun popover tugmasiga aria-expanded yozilmaydi.

ARIA'ning birinchi qoidasi (ARIA asoslari) JavaScript'da ham amal qiladi: tayyor element yetsa — o'shani oling. aria-expanded ni JavaScript bilan boshqarish — tayyor element mos kelmaganda.

Tekshirib ko'ring: Panelni yashirish uchun panel.hidden o'rniga panel.style.display = "none" ishlatildi. Bu Malika uchun nimani o'zgartiradi?

Javob

Hech narsani — ikkalasi ham elementni daraxtdan chiqaradi, ekran o'quvchi yashirin panelni o'qimaydi. Muhimi aria-expanded ni yangilash. Faqat ko'rinishdan berkitib, daraxtda qoldirish (masalan opacity: 0) xato bo'lardi: Malika "yopiq" panelning matnini eshitardi.

3. Jonli hudud: o'zgarishni ovoz chiqarib aytish

3.1 "Savatga qo'shildi"

Jonli hududlar darsida HTML qismini yozgan edik: role="status" li bo'sh element sahifada oldindan turadi, matni o'zgarsa — ekran o'quvchi uni o'zi o'qiydi. "Matnni yozish — JavaScript ishi" degan edik. Mana u:

html
<ul id="menyu">
  <li>Osh — 35 000
    <button type="button" data-taom="Osh">Savatga</button></li>
  <li>Manti — 30 000
    <button type="button" data-taom="Manti">Savatga</button></li>
</ul>
<p id="savat-xabar" role="status"></p>
<script>
  const xabar = document.querySelector("#savat-xabar");
  let soni = 0;

  const menyu = document.querySelector("#menyu");
  menyu.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    soni = soni + 1;
    xabar.textContent =
      `${tugma.dataset.taom} savatga qo'shildi. Savatda ${soni} ta.`;
  });
</script>

"Osh" yonidagi «Savatga» ni bosing — pastda "Osh savatga qo'shildi. Savatda 1 ta." chiqadi. Fokus tugmada qoldi, Malika esa e'lonni eshitadi. Accessibility daraxtida bu p shunday ko'rinadi: role: status, live: polite, atomic: true — role="status" jonli hudud xususiyatlarini o'zi qo'ydi.

Uchta qoida, hammasi Jonli hududlar darsidan:

  • Hudud oldindan, bo'sh holda turadi. Elementni xabar bilan birga yaratib qo'shsangiz, ko'p ekran o'quvchilar uni "yangi hudud" deb e'tiborsiz qoldiradi. JavaScript faqat matnni almashtiradi.
  • polite — Malika hozirgi gapini tugatgach aytiladi. Shoshilinch xato uchun role="alert" (assertive — darhol, gapni bo'lib).
  • Hudud ko'rinmas bo'lmasin. hidden yoki display: none li hudud daraxtda yo'q — undagi o'zgarish aytilmaydi. Ko'zga ko'rinmasligi kerak bo'lsa — korinmas class'i (label: har maydonning nomi).

Matnga "Savatda 2 ta" ni qo'shganimizning ham sababi bor. Bir xil matnni qayta yozsangiz, ba'zi ekran o'quvchilar uni "o'zgarmadi" deb ikkinchi marta aytmaydi. Son har safar o'zgaradi — e'lon ham har safar yangi.

Maslahat: Yangi imkoniyat: document.ariaNotify("Osh savatga qo'shildi") — jonli hududsiz, to'g'ridan-to'g'ri e'lon. web-features ma'lumotiga ko'ra u 2026-yil 14-sentabrdan barcha asosiy brauzerlarda bor (Baseline "yangi"). Eski brauzerlar hali ko'p, shuning uchun hozircha asosiy yo'l — role="status"; ariaNotify ni qo'shimcha sifatida kuzatib boring.

3.2 Qidiruv: har harfda e'lon qilmang

Jonli hududlar darsidagi ikkinchi va'da. Menyuda qidiruv bor: mehmon yozgan sari ro'yxat filtrlanadi va "N ta topildi" e'lon qilinadi. Har harfda e'lon qilsak, Malika "5 ta topildi", "3 ta topildi", "1 ta topildi" ni ketma-ket eshitadi — o'zi yozayotgan harflarni eshitolmay qoladi.

Yechim: ro'yxatni darhol yangilash, e'lonni esa mehmon yozishdan to'xtagach bir marta qilish. Bu — Fokus, scroll va resize hodisalari darsidagi debounce: har harfda eski taymer clearTimeout bilan bekor qilinadi va yangisi qo'yiladi.

html
<label for="qidiruv">Menyudan qidirish</label>
<input id="qidiruv" type="search">
<ul>
  <li>Osh</li>
  <li>Lag'mon</li>
  <li>Manti</li>
  <li>Ko'k choy</li>
  <li>Shashlik</li>
</ul>
<p id="natija" role="status"></p>
<script>
  const qidiruv = document.querySelector("#qidiruv");
  const taomlar = document.querySelectorAll("li");
  const natija = document.querySelector("#natija");
  let taymer;

  qidiruv.addEventListener("input", () => {
    const soz = qidiruv.value.trim().toLowerCase();
    let topildi = 0;
    for (const taom of taomlar) {
      const mos = taom.textContent.toLowerCase().includes(soz);
      taom.hidden = !mos;
      if (mos) {
        topildi = topildi + 1;
      }
    }

    clearTimeout(taymer);
    taymer = setTimeout(() => {
      natija.textContent = `${topildi} ta taom topildi`;
      console.log("E'lon:", natija.textContent);
    }, 600);
  });
</script>

Tez-tez "ma" deb yozing. Ro'yxat har harfda darhol filtrlanadi: "m" da — Lag'mon va Manti, "ma" da — faqat Manti. Konsolda esa e'lon bitta — yozishni to'xtatganingizdan 0,6 soniya keyin: E'lon: 1 ta taom topildi.

Vaqt chizig'ida bu shunday ko'rinadi:

sequenceDiagram
  participant M as Mehmon
  participant J as JavaScript
  participant N as NVDA
  M->>J: "m" yozdi
  J->>J: ro'yxat filtrlandi, taymer 600 ms
  M->>J: "a" yozdi (300 ms o'tib)
  J->>J: eski taymer bekor, yangi 600 ms
  Note over M,J: Mehmon to'xtadi
  J->>N: "1 ta taom topildi"

Qolip resize dagi bilan bir xil, faqat endi input uchun: ish hodisalar oqimi to'xtagach bir marta bajariladi. Uni qidiruvda serverga so'rov yuborishda ham ishlatasiz — har harfda so'rov yubormaslik uchun.

Tekshirib ko'ring: Kodda clearTimeout(taymer); qatori o'chirib tashlandi. Mehmon "ma" deb yozsa, Malika nimani eshitadi?

Javob

Ikkita e'lon: avval "m" uchun ("2 ta taom topildi"), keyin "ma" uchun ("1 ta taom topildi"). Har harf o'z taymerini qo'ydi va hech biri bekor qilinmadi — ular shunchaki 600 ms kechikib, baribir har harfda ishladi.

4. Fokusni boshqarish

4.1 Element o'chdi — fokus qayerga?

Fokusdagi element DOM'dan olib tashlansa, fokus body ga tushadi. Ko'radigan odam buni sezmaydi ham. Malika uchun esa bu — sahifaning boshiga uloqtirilish. Buni o'zingiz ko'ring:

html
<ul id="buyurtma">
  <li>Osh <button type="button">Oshni o'chirish</button></li>
  <li>Manti <button type="button">Mantini o'chirish</button></li>
</ul>
<script>
  const royxat = document.querySelector("#buyurtma");
  royxat.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma !== null) {
      tugma.closest("li").remove();
      console.log("Fokus:", document.activeElement.tagName);
    }
  });
</script>

«Oshni o'chirish» ni bosing — konsolda Fokus: BODY. Qoida: element o'chsa, fokusni o'zingiz mantiqiy joyga qo'ying. Odatda bu — keyingi qatordagi xuddi shunday tugma, u bo'lmasa — oldingisi, ro'yxat bo'shab qolsa — ro'yxat sarlavhasi:

html
<h2 id="sarlavha" tabindex="-1">Buyurtma</h2>
<ul id="buyurtma">
  <li>Osh <button type="button">Oshni o'chirish</button></li>
  <li>Manti <button type="button">Mantini o'chirish</button></li>
  <li>Choy <button type="button">Choyni o'chirish</button></li>
</ul>
<script>
  const sarlavha = document.querySelector("#sarlavha");

  const royxat = document.querySelector("#buyurtma");
  royxat.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const qator = tugma.closest("li");
    const qoshni =
      qator.nextElementSibling ?? qator.previousElementSibling;
    qator.remove();

    if (qoshni !== null) {
      qoshni.querySelector("button").focus();
    } else {
      sarlavha.focus();
    }
    console.log("Fokus:", document.activeElement.textContent);
  });
</script>

«Mantini o'chirish» ni bosing — konsolda Fokus: Choyni o'chirish: fokus keyingi qatorga o'tdi. «Choyni o'chirish» — endi keyingi yo'q, Fokus: Oshni o'chirish. Oxirgisini o'chiring — Fokus: Buyurtma, sarlavha. Malika har safar o'zi turgan joyda qoladi va NVDA yangi fokusni aytadi.

Ikki detal:

  • tabindex="-1" sarlavhaga focus() qilish imkonini beradi, lekin uni Tab tartibiga qo'shmaydi (Klaviatura va fokus boshqaruvi). Fokus faqat dastur qo'yganda tushadi.
  • nextElementSibling ?? previousElementSibling — DOM bo'ylab harakatlanish darsidagi qo'shnilar va ?? operatori. Elementni o'chirishdan oldin qo'shnisini topib olamiz — keyin kech bo'ladi.

4.2 Oyna: fokus ichkariga va orqaga

Ikkinchi qoida: biror narsa ochilsa — fokus unga kirsin, yopilsa — uni ochgan tugmaga qaytsin. dialog va showModal() buni o'zi qiladi (dialog darsi):

html
<button type="button" id="ochish">Bron shartlari</button>
<dialog id="shartlar" aria-labelledby="shartlar-sarlavha">
  <h2 id="shartlar-sarlavha">Bron shartlari</h2>
  <p>Kechiksangiz, stol 15 daqiqa saqlanadi.</p>
  <button type="button" id="yopish">Tushunarli</button>
</dialog>
<script>
  const ochish = document.querySelector("#ochish");
  const oyna = document.querySelector("#shartlar");

  ochish.addEventListener("click", () => {
    oyna.showModal();
    console.log("Ochildi. Fokus:", document.activeElement.id);
  });
  document.querySelector("#yopish").addEventListener("click", () => {
    oyna.close();
  });
  oyna.addEventListener("close", () => {
    console.log("Yopildi. Fokus:", document.activeElement.id);
  });
</script>

«Bron shartlari» ni bosing — konsolda Ochildi. Fokus: yopish: fokus oynadagi yagona tugmaga tushdi. «Tushunarli» yoki Esc — Yopildi. Fokus: ochish. Biz bitta ham focus() yozmadik.

O'zingiz yasagan panel (masalan, div dan yasalgan yon menyu) uchun bularning hammasi qo'lda qilinadi. Qolip shunday:

js
let oldingiFokus = null;

function paneliniOch() {
  oldingiFokus = document.activeElement; // kim ochdi — eslaymiz
  panel.hidden = false;
  panel.querySelector("a, button").focus(); // ichkariga
}

function paneliniYop() {
  panel.hidden = true;
  oldingiFokus?.focus(); // orqaga
}

4.3 Fokus tuzog'i (focus trap)

Modal oyna ochiq paytda Tab uning ichida aylanishi kerak — orqadagi sahifaga "tushib ketmasligi". Bu fokus tuzog'i (focus trap) deyiladi. Malika uchun bu muhim: modal paytida orqadagi havolalar umuman bosilmaydi, ularga fokus tushsa, u qayerdaligini tushunmay qoladi.

showModal() tuzoqni o'zi yasaydi: orqadagi sahifa inert (harakatsiz) bo'ladi. Yuqoridagi oynada sinadik: Tab qayta-qayta bosilganda ham fokus orqadagi «Bron shartlari» tugmasiga bir marta ham tushmadi. Oynadan chiqsa — faqat brauzerning o'z paneliga (manzil satri), dialog darsida aytilganidek.

O'zingiz yasagan panelda xuddi shu ishni inert atributi qiladi (Fokus va tahrir atributlari). U JavaScript'da mantiqiy xususiyat:

js
function paneliniOch() {
  oldingiFokus = document.activeElement;
  document.querySelector("main").inert = true; // orqa — harakatsiz
  panel.hidden = false;
  panel.querySelector("a, button").focus();
}

function paneliniYop() {
  document.querySelector("main").inert = false;
  panel.hidden = true;
  oldingiFokus?.focus();
}

inert element ichidagi hamma narsa bosilmaydi, fokus olmaydi va accessibility daraxtidan chiqadi. Bitta qator — butun tuzoq.

Eski darslarda Tab va Shift + Tab ni ushlab, oxirgi elementdan birinchisiga "sakratadigan" uzun kodlarni uchratasiz. Endi ular kerak emas: inert 2023-yil apreldan barcha asosiy brauzerlarda bor, 2025-yil oktabridan esa "keng tarqalgan" Baseline (web-features). Lekin birinchi tanlov baribir dialog — u Esc ni ham, orqa fonni ham beradi. dialog ning JavaScript tomonini <dialog> va Popover API JS'dan darsida chuqurroq ko'ramiz.

Tekshirib ko'ring: Oyna yopilgach, fokusni qaytarmasdan qoldirsak (o'zingiz yasagan panelda), klaviaturadagi Bobur nimani boshdan kechiradi?

Javob

Panel yopilib, ichidagi fokusli element yashirindi — fokus body ga tushdi. Bobur Tab bosadi va sahifaning boshidan, birinchi havoladan yana yura boshlaydi. Tugma sahifa o'rtasida bo'lsa, unga qaytish uchun o'nlab Tab kerak bo'ladi.

5. Klaviatura: roving tabindex

5.1 G'oya

Klaviatura va fokus boshqaruvi darsida «Bahor» menyusi filtrining HTML'ini yozgan edik: guruhda faqat bitta tugmada tabindex="0", qolganlarida -1. Tab guruhga bir marta kiradi, guruh ichida esa strelkalar yuradi. Bu roving tabindex ("ko'chib yuruvchi tabindex"): 0 qiymati fokus bilan birga tugmadan tugmaga ko'chadi. "Strelkani JavaScript qiladi" degan edik.

Nega Tab bilan hammasini o'tib chiqish yomon? Menyu tablari 6 ta, filtr tugmalari 8 ta bo'lsa — Bobur asosiy kontentga yetguncha 14 marta Tab bosadi. Roving bilan — 2 marta: har guruhga bitta. Guruh ichida strelkalar.

5.2 Menyu tablari jonlanadi

Tabs va accordion darsidagi «Bahor» tablari: HTML va CSS tayyor, "Salatlar" ni bossangiz — hech narsa bo'lmasdi. O'shanda "JavaScript atributni almashtiradi, ko'rinishni CSS o'zi yangilaydi" degan edik. HTML va CSS'ga bitta ham o'zgartirish kiritmaymiz — faqat skript qo'shamiz:

html
<style>
  body { font-family: system-ui, sans-serif; color: #1f2a24; }
  [role="tablist"] {
    display: flex;
    gap: 4px;
    border-bottom: 2px solid #c9d6cf;
  }
  [role="tab"] {
    padding: 10px 14px;
    border: 0;
    border-bottom: 3px solid transparent;
    margin-bottom: -2px;
    background: none;
    color: #5b6660;
    font: inherit;
  }
  [role="tab"]:hover { color: #1f2a24; }
  [role="tab"][aria-selected="true"] {
    border-bottom-color: #2f6b3a;
    color: #2f6b3a;
    font-weight: bold;
  }
  [role="tab"]:focus-visible,
  [role="tabpanel"]:focus-visible {
    outline: 3px solid #1a5d1a;
    outline-offset: 2px;
    box-shadow: 0 0 0 2px #fffaf0;
  }
  [role="tabpanel"] { padding: 12px 4px; }
  [role="tabpanel"] ul { margin: 0; padding-left: 20px; }
</style>
<div role="tablist" aria-label="Menyu bo'limlari">
  <button type="button" role="tab" id="tab-issiq"
          aria-selected="true"
          aria-controls="panel-issiq">Issiq taomlar</button>
  <button type="button" role="tab" id="tab-salat"
          aria-selected="false" tabindex="-1"
          aria-controls="panel-salat">Salatlar</button>
  <button type="button" role="tab" id="tab-ichimlik"
          aria-selected="false" tabindex="-1"
          aria-controls="panel-ichimlik">Ichimliklar</button>
</div>
<div role="tabpanel" id="panel-issiq"
     aria-labelledby="tab-issiq" tabindex="0">
  <ul>
    <li>Osh — 35 000</li>
    <li>Lag'mon — 28 000</li>
  </ul>
</div>
<div role="tabpanel" id="panel-salat"
     aria-labelledby="tab-salat" tabindex="0" hidden>
  <ul><li>Vinegret — 15 000</li></ul>
</div>
<div role="tabpanel" id="panel-ichimlik"
     aria-labelledby="tab-ichimlik" tabindex="0" hidden>
  <ul><li>Ko'k choy (choynak) — 5 000</li></ul>
</div>
<script>
  const royxat = document.querySelector('[role="tablist"]');
  const tablar = [...royxat.querySelectorAll('[role="tab"]')];

  function tabniTanla(yangi) {
    for (const tab of tablar) {
      const tanlandi = tab === yangi;
      tab.setAttribute("aria-selected", String(tanlandi));
      tab.tabIndex = tanlandi ? 0 : -1;
      const panelId = tab.getAttribute("aria-controls");
      document.getElementById(panelId).hidden = !tanlandi;
    }
    yangi.focus();
    console.log("Tanlandi:", yangi.textContent);
  }

  royxat.addEventListener("click", (event) => {
    const tab = event.target.closest('[role="tab"]');
    if (tab !== null) {
      tabniTanla(tab);
    }
  });

  royxat.addEventListener("keydown", (event) => {
    const i = tablar.indexOf(document.activeElement);
    const oxirgi = tablar.length - 1;
    let yangi;
    if (event.key === "ArrowRight") {
      yangi = i === oxirgi ? 0 : i + 1;
    } else if (event.key === "ArrowLeft") {
      yangi = i === 0 ? oxirgi : i - 1;
    } else if (event.key === "Home") {
      yangi = 0;
    } else if (event.key === "End") {
      yangi = oxirgi;
    } else {
      return;
    }
    event.preventDefault();
    tabniTanla(tablar[yangi]);
  });
</script>

Endi Bobur kabi sinab ko'ring — sichqonchasiz:

  1. "Issiq taomlar" tabini bir marta bosing (yoki Tab bilan unga keling).
  2. → — "Salatlar" tanlandi, panel almashdi, konsolda Tanlandi: Salatlar. Yana → — "Ichimliklar". Yana → — aylanib "Issiq taomlar" ga qaytdi.
  3. End — oxirgi tab, Home — birinchisi.
  4. Tab bosing — fokus tablar orasida emas, to'g'ridan-to'g'ri panelga o'tadi. Uchala tab — bitta Tab to'xtashi.

Accessibility daraxtida tanlangan tab selected: true bo'ladi, qolganlari false. NVDA "Salatlar, tab, tanlangan, 2 dan 2" kabi aytadi — tartib raqamini daraxtning o'zidan oladi.

Kodni o'qiymiz:

  • tabniTanla — yagona "chizuvchi" funksiya. Uch narsa birga o'zgaradi: aria-selected (ekran o'quvchi va CSS uchun), tabIndex (roving) va panel hidden. Sichqoncha ham, klaviatura ham shu funksiyani chaqiradi — ikki yo'l hech qachon ajralib qolmaydi.
  • tabIndex — tabindex atributining JavaScript xususiyati, katta I bilan (readOnly kabi).
  • [...royxat.querySelectorAll(...)] — NodeList ni massivga aylantirdik (spread), chunki indexOf massiv metodi.
  • event.preventDefault() — Home va End odatda sahifani boshiga yoki oxiriga aylantiradi. Tab ichida bu kerak emas. Biz faqat o'zimiz ishlatgan tugmalarda to'xtatamiz — boshqa tugmalar (masalan Tab) odatdagidek ishlaydi.

Strelka bosilganda tab darhol tanlanadi — bu "avtomatik faollashtirish". ARIA Authoring Practices yana bir variantni ham beradi: strelka faqat fokusni ko'chiradi, tanlov esa Enter yoki Space bilan. Panel mazmuni sekin yuklanadigan bo'lsa (serverdan), ikkinchisi yaxshiroq.

Tekshirib ko'ring: keydown tinglovchisi har bir tabga emas, tablist ga qo'yilgan. Nega bu ishlaydi va nima uchun qulay?

Javob

keydown fokusdagi tabda chiqadi va tablist gacha ko'tariladi (Event delegation). Qaysi tabdan kelganini document.activeElement aytadi. Qulayligi: tab qo'shilsa yoki olib tashlansa, tinglovchini qayta ulash shart emas, bitta tinglovchi uchala tabga xizmat qiladi.

6. Ko'p uchraydigan xatolar

6.1 Holat atributi yangilanmadi

Ko'rinish o'zgardi, aria-expanded yoki aria-selected esa eskicha. Tuzatish: ko'rinishni atributdan chiqaring yoki ikkalasini bitta funksiyada o'zgartiring («Menyu tablari jonlanadi» dagi tabniTanla kabi). Tekshiruv — Accessibility yorlig'i.

6.2 Jonli hudud xabar bilan birga yaratildi

const p = document.createElement("p"); p.setAttribute("role", "status"); p.textContent = "..."; document.body.append(p); — ko'p ekran o'quvchilar bunday "tug'ilgan zahoti to'la" hududni aytmaydi. Tuzatish: hudud HTML'da oldindan, bo'sh; JavaScript faqat matnni o'zgartiradi.

6.3 Har harfda e'lon

input da to'g'ridan-to'g'ri natija.textContent = ... — Malika har harfda yangi son eshitadi. Tuzatish: debounce — clearTimeout + setTimeout.

6.4 role="alert" hamma joyda

"Savatga qo'shildi" uchun alert — Malika gapining o'rtasida bo'linadi. Tuzatish: oddiy xabar — status; alert — faqat darhol bilish shart bo'lgan xato ("To'lov o'tmadi").

6.5 Fokusdagi elementni o'chirish yoki yashirish

remove(), hidden = true yoki tugmani disabled qilish — fokus body ga tushadi. Oxirgisi kutilmagan. Chrome'da sinadik: fokusdagi tugma disabled bo'lgach, document.activeElement — BODY. Tuzatish: fokusni oldindan qo'shniga ko'chiring; vaqtincha bosilmaydigan tugma uchun disabled o'rniga aria-disabled="true" va kodda tekshiruv («Portfolio qadami» da shunday qilamiz).

6.6 Rol bor, klaviatura yo'q

role="tab" yozildi, strelkalar esa ishlamaydi. Ekran o'quvchi "tab" deydi — Malika strelka bosadi — hech narsa bo'lmaydi. Bu rolsiz holatdan ham yomonroq (ARIA asoslari). Tuzatish: rolni yozdingizmi — uning klaviatura qoidalarini ham yozing.

7. Mashqlar

1-mashq (oson): Kim nimani eshitadi?

Har vaziyatda Malika nimani eshitishini (yoki eshitmasligini) va qanday tuzatishni yozing:

  1. "Sevimlilarga qo'shish" bosilganda ikonka qizil yurakka aylandi, boshqa hech narsa o'zgarmadi.
  2. Forma yuborilgach, forma o'rniga "Rahmat!" matni yangi p bo'lib qo'shildi.
  3. "Batafsil" tugmasi details ning summary si.
Yechim
  1. Hech narsa — o'zgarish faqat ko'rinishda. Tuzatish: tugma toggle bo'lsa — aria-pressed="true"/"false" ni yangilash (ARIA asoslari); fokus tugmada, NVDA yangi holatni aytadi.
  2. Ehtimol hech narsa: hudud xabar bilan birga paydo bo'ldi, fokus esa yo'qolgan forma bilan birga body ga tushdi. Tuzatish: oldindan turgan role="status" ga yozish va fokusni mantiqiy joyga qo'yish.
  3. "Batafsil, tugma, yig'ilgan" / "yoyilgan" — details holatni o'zi beradi, JavaScript kerak emas.

2-mashq (o'rta): O'chirish va e'lon

«Element o'chdi — fokus qayerga?» dagi to'g'ri misolga jonli hudud qo'shing: taom o'chirilganda "Manti buyurtmadan olib tashlandi. 2 ta qoldi." deb e'lon qilinsin. Taom nomini qatordagi birinchi matn tugunidan oling (qator.firstChild.textContent.trim(), DOM daraxti).

Yechim

HTML'ga, ro'yxatdan keyin: <p id="buyurtma-xabar" role="status"></p>. Skriptdagi tinglovchi:

html
<h2 id="sarlavha" tabindex="-1">Buyurtma</h2>
<ul id="buyurtma">
  <li>Osh <button type="button">Oshni o'chirish</button></li>
  <li>Manti <button type="button">Mantini o'chirish</button></li>
  <li>Choy <button type="button">Choyni o'chirish</button></li>
</ul>
<p id="buyurtma-xabar" role="status"></p>
<script>
  const royxat = document.querySelector("#buyurtma");
  const xabar = document.querySelector("#buyurtma-xabar");

  royxat.addEventListener("click", (event) => {
    const tugma = event.target.closest("button");
    if (tugma === null) {
      return;
    }
    const qator = tugma.closest("li");
    const nomi = qator.firstChild.textContent.trim();
    const qoshni =
      qator.nextElementSibling ?? qator.previousElementSibling;
    qator.remove();

    const qoldi = royxat.children.length;
    xabar.textContent =
      `${nomi} buyurtmadan olib tashlandi. ${qoldi} ta qoldi.`;
    if (qoshni !== null) {
      qoshni.querySelector("button").focus();
    } else {
      document.querySelector("#sarlavha").focus();
    }
  });
</script>

«Mantini o'chirish» — pastda "Manti buyurtmadan olib tashlandi. 2 ta qoldi.", fokus «Choyni o'chirish» da. Malika ikkalasini eshitadi: avval yangi fokusni, keyin (polite — navbat bilan) e'lonni.

3-mashq (qiyin): Harf bilan sakrash

Tooltip, dropdown va toast darsida va'da qilingan uchinchi klaviatura imkoniyati — harf bilan qidirish (type-ahead): ro'yxatda harf bosilsa, fokus shu harf bilan boshlanadigan keyingi elementga o'tadi. «Menyu tablari jonlanadi» dagi tablarga qo'shing: S — "Salatlar", I — navbatma-navbat "Issiq taomlar" va "Ichimliklar".

Ishora: event.key.length === 1 — bitta belgili tugma (harf). Tab nomi shu harf bilan boshlanadimi — startsWith aytadi (Matn ichida qidirish); mosini topish — find (Massivda qidirish). Joriy tabdan keyingi tablardan boshlab qidiring, oxiriga yetsangiz — boshidan (Kesish, birlashtirish va matnga aylantirish darsidagi slice va concat bilan "aylanma" ro'yxat yasash mumkin).

Yechim

keydown tinglovchisidagi oxirgi else tarmog'ini almashtiring:

js
} else if (event.key.length === 1) {
  const harf = event.key.toLowerCase();
  const navbat = tablar.slice(i + 1).concat(tablar.slice(0, i + 1));
  const topilgan = navbat.find((tab) =>
    tab.textContent.toLowerCase().startsWith(harf));
  if (topilgan === undefined) {
    return;
  }
  yangi = tablar.indexOf(topilgan);
} else {
  return;
}

"Issiq taomlar" da turib I bosing — "Ichimliklar" (navbat keyingisidan boshlanadi). Yana I — "Issiq taomlar" ga qaytadi. S — "Salatlar". Mos harf bo'lmasa (Q) — hech narsa o'zgarmaydi va preventDefault chaqirilmaydi. navbat — joriy tabdan keyingilar, keyin boshidagilar: bir xil harf bilan boshlanuvchi tablar orasida aylanish shu tufayli ishlaydi.

4-mashq: Portfolio qadami — menyu holati va natija e'loni

Portfolio'da ikki joy JavaScript bilan o'zgaradi: mobil menyu (addEventListener asoslari darsidagi popover va hidePopover) va aloqa formasining #aloqa-natija xabari (FormData). Ikkalasini Malika uchun tekshiring va tuzating.

  1. Menyu tugmasi. Telefon kengligida DevTools → Elements → «Menyu» tugmasi → Accessibility yorlig'i. Menyuni oching-yoping va Expanded qatorini kuzating. Tugmaga aria-controls="asosiy-menyu" qo'shing. aria-expanded ni qo'lda yozmang — popover uni o'zi beradi («Avval tayyor element» dagi fakt).
  2. Natija e'loni. aloqa/index.html dagi <p id="aloqa-natija"></p> ga role="status" qo'shing. Element sahifada oldindan, bo'sh turibdi — jonli hudud qoidasiga mos. "Yuborilmoqda..." va "Rahmat, ...!" endi o'qiladi.
  3. Yo'qolgan fokus. FormData darsida yuborish paytida tugmani disabled qilgan edik. Bu fokusni body ga tashlaydi («Fokusdagi elementni o'chirish yoki yashirish» xatosi). disabled ni aria-disabled="true" ga almashtiring va kodda "hozir yuborilyaptimi?" degan bayroq bilan takroriy yuborishni to'xtating.
  4. Accessibility yorlig'ida tekshiring, keyin commit: A11y: menyu holati, natija e'loni, fokus.
Yechim

Menyu tugmasi (header'da, popovertarget yonida):

html
<button type="button" popovertarget="asosiy-menyu"
        aria-controls="asosiy-menyu">Menyu</button>

Chrome 154 da tekshirdik: popover yopiq — expanded: false, ochiq — expanded: true; aria-controls daraxtga controls: asosiy-menyu bog'lanishini qo'shadi. Agar menyuni popover'siz, o'zingiz JavaScript bilan ochadigan qilgan bo'lsangiz — «Ochiladigan panel» dagi kabi aria-expanded ni har bosishda yangilang.

Natija paragrafi:

html
<p id="aloqa-natija" role="status"></p>

aloqaFormasiniUla dagi submit tinglovchisi:

js
let yuborilmoqda = false;

forma.addEventListener("submit", (event) => {
  event.preventDefault();
  if (yuborilmoqda) {
    return; // takroriy bosish yoki Enter
  }
  yuborilmoqda = true;
  const tugma = event.submitter;
  tugma?.setAttribute("aria-disabled", "true");

  const malumot = Object.fromEntries(new FormData(forma));
  malumot.ism = malumot.ism.trim();
  natija.textContent = "Yuborilmoqda...";

  setTimeout(() => {
    natija.textContent =
      `Rahmat, ${malumot.ism}! Xabaringiz qabul qilindi. ` +
      "(Hozircha namuna: xabar hech qayerga yuborilmadi.)";
    forma.reset();
    hisobniYangila();
    tugma?.removeAttribute("aria-disabled");
    yuborilmoqda = false;
  }, 800);
});

Natijani tekshiring — tugmani tez-tez bosing:

html
<form action="https://example.com/aloqa" method="post">
  <p><label for="ism">Ismingiz</label>
    <input id="ism" name="ism" value="Ali" required></p>
  <button type="submit">Yuborish</button>
</form>
<p id="aloqa-natija" role="status"></p>
<script>
  const forma = document.querySelector("form");
  const natija = document.querySelector("#aloqa-natija");
  let yuborilmoqda = false;

  forma.addEventListener("submit", (event) => {
    event.preventDefault();
    if (yuborilmoqda) {
      return;
    }
    yuborilmoqda = true;
    const tugma = event.submitter;
    tugma?.setAttribute("aria-disabled", "true");
    const malumot = Object.fromEntries(new FormData(forma));
    natija.textContent = "Yuborilmoqda...";
    console.log("Yuborildi:", malumot.ism);

    setTimeout(() => {
      natija.textContent = `Rahmat, ${malumot.ism.trim()}!`;
      tugma?.removeAttribute("aria-disabled");
      yuborilmoqda = false;
      console.log("Fokus:", document.activeElement.tagName);
    }, 800);
  });
</script>

Konsolda bitta Yuborildi: Ali (takroriy bosishlar bayroqqa urildi), keyin Fokus: BUTTON — fokus tugmada qoldi. aria-disabled tugmani fokusdan chiqarmaydi va ekran o'quvchiga "nofaol" deydi, lekin bosishni to'xtatmaydi — shuning uchun yuborilmoqda bayrog'i kerak. Ko'rinishi uchun komponentlar.css ga button[aria-disabled="true"] { opacity: 0.6; cursor: progress; } qo'shishingiz mumkin.

8. Real ishda

  • Savat, bildirishnoma, "saqlandi" yozuvi — har online do'konda role="status". Tekshiruvchilar (QA) birinchi bo'lib "ekran o'quvchi buni aytadimi?" deb so'raydi.
  • Komponent kutubxonalari — Radix, React Aria, Headless UI (kursda alohida o'rganamiz, hozir bilish shart emas) — aynan bugungi qoidalarni tayyor qilib beradi: roving tabindex, fokusni qaytarish, aria-expanded. Ularni to'g'ri tanlash va tekshirish uchun ostidagi mexanizmni bilish kerak.
  • Qonun va shartnomalar. Evropa Ittifoqida 2025-yil iyunidan Evropa qulaylik akti kuchga kirdi — ko'p online xizmatlar WCAG ga mos bo'lishi shart. Xalqaro buyurtmachilar bilan ishlaganda "a11y" talablari shartnomaga yoziladi.
  • Avtomatik tekshiruv yetmaydi. Lighthouse (DevTools II: Lighthouse) aria-expanded yangilanmaganini yoki fokus yo'qolganini topmaydi — bular faqat harakatda ko'rinadi. Shuning uchun real jamoalarda klaviatura va ekran o'quvchi bilan qo'lda sinov o'tkaziladi.
  • Intervyuda: "aria-live qanday ishlaydi?", "Modal yopilgach fokus qayerga borishi kerak?", "Roving tabindex nima?" — uchalasiga bugun javob berdingiz.

Xulosa

  • JavaScript o'zgartirgan narsa ekran o'quvchiga faqat ikki yo'l bilan yetadi: fokusdagi element va jonli hudud.
  • Holat — atributda: aria-expanded, aria-selected, aria-pressed ko'rinish bilan birga, bitta funksiyada yangilanadi; tayyor element (details, popover) bo'lsa — o'sha.
  • Jonli hudud oldindan, bo'sh turadi; JavaScript faqat matnni o'zgartiradi; oddiy xabar — status, shoshilinch xato — alert; qidiruvda — debounce.
  • Fokus hech qachon yo'qolmaydi: o'chirilgan element o'rniga qo'shnisi, yopilgan oynadan — ochgan tugma; disabled ham fokusni tashlaydi.
  • Modal — dialog + showModal() (fokus tuzog'i tekin); o'z paneli — inert.
  • Tablar va asboblar paneli — roving tabindex: guruhga bitta Tab, ichida strelka, Home, End.

Keyingi dars: Debugging metodologiyasi — kod kutilganidek ishlamasa, xatoni tasodifan emas, tizimli usul bilan topishni o'rganamiz.

Manbalar

  • W3C: "ARIA Authoring Practices Guide" — Disclosure, Tabs, Dialog (Modal) patternlari va "Developing a Keyboard Interface" — w3.org/WAI/ARIA/apg
  • MDN: "ARIA live regions", "aria-expanded", "HTMLElement: inert", "HTMLDialogElement: showModal()" — developer.mozilla.org
  • web-features (Baseline): inert, arianotify, aria-attribute-reflection — 2026-10 holati
  • Chrome DevTools: "Accessibility features reference" — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Dinamik UI'da qulaylik (a11y): aria-live, fokus va aria-expanded JavaScript bilan — IlmHamroh