Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Modal oyna: fokusni ichkariga olib kirish va qaytarish
- 2.1 Uch qoida
- 2.2 React'da: ref + effekt
- 2.3 Focus trap qo'lda: aylanish hisobi
- 2.4 Native <dialog> va inert
- 3. Roving tabindex: bitta to'xtash, ichida strelkalar
- 3.1 Muammo: 20 ta Tab
- 3.2 Texnika
- 3.3 vazifalar dagi variant: Menyu
- 4. Sahifa darajasida fokus
- 4.1 Skip link
- 4.2 SPA'da "sahifa" almashganda
- 5. aria-live: fokus siljimaydigan xabarlar
- 6. Vazifalar qadami: «Ro'yxatga o'tish» va Bobur ssenariysi
- 6.1 Skip link
- 6.2 Bobur — haqiqiy ssenariy
- 6.3 Commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi texnika?
- 2-mashq (o'rta): o'chirilgan qatordan keyin fokus
- 3-mashq (qiyin): radio guruh — roving tabindex
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
Fokus boshqaruvi va klaviatura navigatsiyasi: modal, roving tabindex, skip link va inert
Qisqacha: React DOM'ni o'zgartirganda fokus haqida o'ylamaydi: element o'chsa, fokus
<body>ga tushib qoladi va klaviatura foydalanuvchisi "adashadi". Shuning uchun fokusni o'zingiz boshqarasiz: modal ochilganda — ichiga, yopilganda — ochgan tugmaga qaytarish; modal ichida Tab aylansin (focus trap), orqadagi sahifa esainertbo'lsin. Ro'yxat va menyularda Tab bitta to'xtash, ichida — strelkalar (roving tabindex). Sahifa boshida — "Asosiy qismga o'tish" (skip link). Fokus siljimasa ham muhim xabar —aria-livehudud orqali.
Bu darsda
- Modal oynada fokusni ichkariga olib kirasiz va yopilganda ochgan tugmaga qaytarasiz.
- Focus trap'ni qo'lda va native
<dialog>+inertbilan qilasiz, farqini tushuntira olasiz. - Roving tabindex bilan strelka navigatsiyasini yozasiz.
- Skip link qo'shasiz va SPA'da "sahifa" almashganda fokusni sarlavhaga qo'yasiz.
vazifalarga «Ro'yxatga o'tish» havolasini qo'shasiz va Boburning klaviatura ssenariysini o'qiysiz.
Oldin bilishingiz kerak: React'da accessibility asoslari, useRef II, Klaviatura va fokus boshqaruvi, useLayoutEffect.
1. Nega bu kerak?
Oldingi darsda har elementga to'g'ri teg va nom berdik. Lekin dinamik ilovada yana bir muammo bor — fokus qayerda.
Bobur «Bahor» saytida taomni o'chirmoqchi. Tab bilan «O'chirish» ga keldi, Enter bosdi. Tasdiq oynasi ochildi — lekin fokus orqadagi sahifada qoldi. Bobur Tab bosaveradi, fokus oyna orqasidagi ko'rinmaydigan tugmalarda yuradi. Oxiri oynani sichqonchasiz yopolmay, sahifani yangiladi.
Ikkinchi holat: Bobur vazifani o'chirdi. Qator yo'qoldi — u bilan birga fokusdagi tugma ham. Brauzer fokusni <body> ga tashladi. Keyingi Tab — sahifaning eng boshidan, 30 ta to'xtashdan keyin yana ro'yxatga.
Hayotiy o'xshatish: mehmonxona yo'lagidagi gid. U sizni xonaga olib kiradi, eshikni ko'rsatadi, chiqqaningizda qayerda to'xtaganingizni eslaydi. Fokus — klaviatura foydalanuvchisining "gid"i. Ko'zi ko'radigan odam sahifaning istalgan joyiga qaraydi. Bobur esa faqat fokus turgan joyni "ko'radi". Malika uchun esa fokus — qaysi elementni eshitayotgani.
2. Modal oyna: fokusni ichkariga olib kirish va qaytarish
2.1 Uch qoida
Modal oyna uchun WAI-ARIA APG (Authoring Practices Guide — tayyor naqshlar to'plami, ARIA naqshlari) uch qoida beradi:
- Ochilganda — fokus oyna ichiga: birinchi fokuslanadigan elementga yoki eng xavfsiz tugmaga.
- Ochiq paytda — Tab va Shift+Tab oyna ichida aylanadi, orqadagi sahifaga chiqmaydi. Esc — yopadi.
- Yopilganda — fokus oynani ochgan elementga qaytadi.
Jarayonni ketma-ketlik diagrammasida ko'ring:
sequenceDiagram
participant B as Bobur
participant T as «O'chirish» tugmasi
participant M as Modal
B->>T: Enter
T->>M: ochiladi
M->>M: fokus «Bekor qilish» ga
B->>M: Tab, Tab: ichida aylanadi
B->>M: Esc
M->>T: yopiladi, fokus qaytadiPastki o'q — eng ko'p unutiladigan qadam. Fokus qaytmasa, Bobur sahifaning boshidan qayta boshlaydi.
2.2 React'da: ref + effekt
Fokusni qo'yish — DOM bilan ish, demak ref va effekt. Ochgan tugmani esa ochish paytida eslab qolamiz:
import { useEffect, useRef, useState } from "react";
function ConfirmModal({ onClose }) {
const cancelRef = useRef(null);
useEffect(() => {
cancelRef.current.focus(); // 1-qoida: ichkariga
console.log("fokus:", document.activeElement.textContent);
}, []);
return (
<div role="dialog" aria-modal="true" aria-labelledby="sarlavha">
<h2 id="sarlavha">Osh o'chirilsinmi?</h2>
<button ref={cancelRef} onClick={onClose}>
Bekor qilish
</button>{" "}
<button onClick={onClose}>Ha, o'chirish</button>
</div>
);
}
export default function App() {
const [open, setOpen] = useState(false);
const openerRef = useRef(null);
function close() {
setOpen(false);
openerRef.current?.focus(); // 3-qoida: qaytarish
console.log("fokus:", document.activeElement.textContent);
}
return (
<div>
<button
onClick={(e) => {
openerRef.current = e.currentTarget; // kim ochdi
setOpen(true);
}}
>
O'chirish
</button>
{open && <ConfirmModal onClose={close} />}
</div>
);
}Konsolda:
fokus: Bekor qilish
fokus: O'chirishEkranda:
O'chirishUch narsaga qarang:
- Fokus «Bekor qilish» ga, «Ha, o'chirish» ga emas. O'chirish — qaytarib bo'lmaydigan amal. Tasodifiy Enter xavfsiz tugmani bossin.
openerRef.current = e.currentTarget— ochgan elementni ref'da saqlaymiz. State emas, chunki bu qiymat ekranga ta'sir qilmaydi (useRef I).role="dialog"vaaria-modal="true"— Malikaga "bu oyna, orqasi faol emas" deydi.aria-labelledby— oyna nomi sarlavhadan.
Bu demo ataylab soddalashtirilgan: unda 2-qoida (Tab aylanishi) va Esc yo'q. Ularni qo'lda yozish mumkin, lekin keyingi bo'limda ko'rasiz — brauzer buni tayyor beradi.
2.3 Focus trap qo'lda: aylanish hisobi
Focus trap (fokus tuzog'i) — Tab oxirgi elementdan keyin birinchisiga, Shift+Tab birinchisidan oxirgisiga o'tadi. Mantiqning yuragi — kichik sof funksiya, uni alohida sinash oson:
function nextFocus(index, count, shift) {
if (shift) return index === 0 ? count - 1 : index - 1;
return index === count - 1 ? 0 : index + 1;
}
// Modalda 3 ta element: [maydon, Bekor qilish, Saqlash]
console.log(nextFocus(2, 3, false)); // 0
console.log(nextFocus(0, 3, true)); // 2
console.log(nextFocus(1, 3, false)); // 2Komponentda u onKeyDown ichida ishlatiladi: event.key === "Tab" bo'lsa — event.preventDefault(), fokuslanadigan elementlar ro'yxatini (querySelectorAll) olish va list[nextFocus(...)].focus(). Haqiqiy loyihada qiyin joylar ko'p: disabled elementlar, yashirin elementlar, iframe, dinamik qo'shilgan tugmalar. Shuning uchun qo'lda trap — oxirgi chora. Birinchi tanlov — native <dialog>.
2.4 Native <dialog> va inert
Native modal oyna darsidagi <dialog> ning showModal() usuli uchala qoidaning ikkitasini o'zi bajaradi:
- orqadagi butun sahifani
inertqiladi — fokus, bosish va ekran o'quvchi u yerga kira olmaydi; - Esc bosilsa — oyna yopiladi;
- ochilganda fokusni birinchi fokuslanadigan elementga qo'yadi.
Faqat 3-qoida (ochgan tugmaga qaytish) — zamonaviy brauzerlarda bu ham avtomatik, lekin o'zingiz tekshiring. Ko'rish uchun quyidagi blokni saytda ishga tushiring (bu tekshiruvchimizning soddalashtirilgan brauzerida showModal yo'q, shuning uchun u faqat saytda ishlaydi):
import { useRef, useState } from "react";
export default function App() {
const dialogRef = useRef(null);
const [answer, setAnswer] = useState("");
return (
<div>
<button onClick={() => dialogRef.current.showModal()}>
O'chirish
</button>
<button>Orqadagi tugma</button>
<p>{answer}</p>
<dialog
ref={dialogRef}
aria-labelledby="dlg-sarlavha"
onClose={(e) => {
setAnswer("javob: " + e.currentTarget.returnValue);
}}
>
<form method="dialog">
<h2 id="dlg-sarlavha">Osh o'chirilsinmi?</h2>
<button value="bekor">Bekor qilish</button>{" "}
<button value="ochir">Ha, o'chirish</button>
</form>
</dialog>
</div>
);
}Natija oynasida «O'chirish» ni bosing va Tab bosib ko'ring: fokus faqat ikki tugma va brauzer manzil qatori orasida yuradi — «Orqadagi tugma» ga yetmaydi. Esc — yopadi, javob: (bo'sh returnValue). method="dialog" li forma tugmasi oynani yopadi va returnValue — tugmaning value si. vazifalar dagi OchirishDialog aynan shunday (13-qismdan beri).
inert atributini o'zingiz ham qo'llay olasiz — masalan, o'z yon paneli (drawer) ochilganda asosiy qismni o'chirish uchun. React 19 uni boolean prop sifatida biladi:
import { useState } from "react";
export default function App() {
const [open, setOpen] = useState(false);
return (
<div>
<main inert={open}>
<button onClick={() => setOpen(true)}>Savat</button>
</main>
{open && (
<aside aria-label="Savat">
<p>Osh × 2</p>
<button
onClick={() => {
const main = document.querySelector("main");
console.log("inert:", main.hasAttribute("inert"));
setOpen(false);
}}
>
Yopish
</button>
</aside>
)}
</div>
);
}Konsolda:
inert: trueEkranda:
Savat«Savat» ochilganda <main> da inert atributi bor edi, «Yopish» dan keyin u yo'qoldi. inert={open} — true bo'lsa, DOM'da inert="" atributi, false bo'lsa — umuman yo'q. React 18 da inert noma'lum atribut edi va inert="" satr bilan yozilardi. React 19 da boolean. <main> ichidagi hamma narsa endi Tab, bosish va ekran o'quvchidan yashirin — aynan modal orqasi kerak bo'lgan holat.
Tekshirib ko'ring: Modal yopilganda fokusni qaytarish uchun
document.activeElementni ochish paytida eslab qolsa bo'ladimi,e.currentTargeto'rniga?
Javob
Ko'pincha bo'ladi: klaviatura bilan Enter bosilganda fokus tugmada edi. Lekin sichqoncha bilan bosilganda ba'zi brauzerlar (Safari, macOS'da) tugmaga fokus bermaydi — activeElement <body> bo'lib qoladi. Modal ochilishi kod orqali (taymer, server javobi) bo'lsa — ham. e.currentTarget doim aniq: bosilgan element. Native <dialog> qaytishni o'zi boshqaradi, qo'lda modal uchun esa — aniq ref.
3. Roving tabindex: bitta to'xtash, ichida strelkalar
3.1 Muammo: 20 ta Tab
Filtr tugmalari: «Hammasi», «Bajarilmagan», «Bajarilgan» va yana teglar — 10 ta. Har biri Tab to'xtashi bo'lsa, Bobur ro'yxatga yetish uchun 10 marta Tab bosadi. APG naqshi (toolbar, tablist, menyu, radio guruh): guruh — bitta Tab to'xtash, ichida — strelkalar.
3.2 Texnika
Guruhdagi faqat bitta element tabIndex={0} (Tab unga keladi), qolganlari tabIndex={-1} (Tab o'tkazib yuboradi, lekin .focus() bilan fokuslanadi). Strelka bosilganda — "faol" indeks o'zgaradi va fokus yangi elementga ko'chadi. Avval sof hisob:
function nextRoving(key, index, count) {
switch (key) {
case "ArrowRight":
return (index + 1) % count;
case "ArrowLeft":
return (index - 1 + count) % count;
case "Home":
return 0;
case "End":
return count - 1;
default:
return index;
}
}
console.log(nextRoving("ArrowRight", 2, 3)); // 0
console.log(nextRoving("ArrowLeft", 0, 3)); // 2
console.log(nextRoving("End", 0, 3)); // 2
console.log(nextRoving("a", 1, 3)); // 1Aylanma: oxirgidan o'ngga — birinchiga (qoldiq operatori). Endi komponent:
import { useRef, useState } from "react";
const FILTERS = ["Hammasi", "Bajarilmagan", "Bajarilgan"];
function nextRoving(key, index, count) {
if (key === "ArrowRight") return (index + 1) % count;
if (key === "ArrowLeft") return (index - 1 + count) % count;
if (key === "Home") return 0;
if (key === "End") return count - 1;
return index;
}
export default function App() {
const [active, setActive] = useState(0);
const refs = useRef([]);
function handleKeyDown(e) {
const next = nextRoving(e.key, active, FILTERS.length);
if (next !== active) {
e.preventDefault();
setActive(next);
refs.current[next].focus();
}
}
return (
<div role="toolbar" aria-label="Filtr" onKeyDown={handleKeyDown}>
{FILTERS.map((name, i) => (
<button
key={name}
ref={(el) => {
refs.current[i] = el;
}}
tabIndex={i === active ? 0 : -1}
aria-pressed={i === active}
onClick={() => {
setActive(i);
const t = refs.current.map((b) => b.tabIndex).join(" ");
console.log("tabIndex:", t);
}}
>
{name}
</button>
))}
</div>
);
}Konsolda:
tabIndex: 0 -1 -1Ekranda:
HammasiBajarilmaganBajarilganKonsolda bosish paytidagi holat: hali «Hammasi» faol. Vaqt chizig'i:
| Qadam | active |
DOM'dagi tabIndex |
|---|---|---|
«Bajarilgan» bosildi, onClick ichida (setActive(2) chaqirildi) |
hali 0 | 0 -1 -1 — konsolga shu chiqdi |
| ishlovchi tugadi → render → commit | 2 | -1 -1 0 |
setActive qiymatni darhol emas, keyingi render'da o'zgartiradi. Commit'dan keyin Tab endi «Bajarilgan» ga keladi. Natija oynasida o'zingiz sinang: Tab bilan guruhga kiring, ← → bilan yuring, Tab bilan chiqing. onKeyDown guruhda bitta — hodisa tugmadan <div> ga ko'tariladi (Hodisa tarqalishi).
3.3 vazifalar dagi variant: Menyu
vazifalar dagi Menyu (Headless komponentlar) shu g'oyaning ikkinchi shaklidan foydalanadi: bandlar hammasi tabIndex: -1, strelka bilan fokus to'g'ridan-to'g'ri ko'chadi, Tab esa menyuni yopadi va fokus tabiiy davom etadi. Menyu vaqtinchalik (ochiladi va yopiladi), shuning uchun "eslab qolinadigan" tabIndex={0} kerak emas. Toolbar esa doimiy — u oxirgi tanlovni eslaydi. Uchinchi texnika — aria-activedescendant: fokus konteynerda qoladi, "faol" element id bilan ko'rsatiladi. U combobox va katta ro'yxatlarda qulay.
4. Sahifa darajasida fokus
4.1 Skip link
Har sahifa boshida bir xil narsalar: logo, menyu, qidiruv, filtrlar. Bobur har safar ularni Tab bilan bosib o'tadi. Skip link — sahifadagi birinchi havola, "Asosiy qismga o'tish". U faqat fokusda ko'rinadi (WCAG 2.4.1 "Bypass Blocks"). vazifalar da u ro'yxat sarlavhasiga olib boradi — «Vazifalar qadami» bo'limida kanon kodi. Uning ishlash mexanizmi:
import { useRef } from "react";
export default function App() {
const headingRef = useRef(null);
return (
<div>
<a
href="#royxat"
className="otish"
onClick={(e) => {
e.preventDefault();
headingRef.current.focus();
console.log("fokus:", document.activeElement.textContent);
console.log("URL hash:", JSON.stringify(location.hash));
}}
>
Ro'yxatga o'tish
</a>
<nav>Logo · Menyu · Qidiruv · Filtrlar</nav>
<h2 id="royxat" ref={headingRef} tabIndex={-1}>
Ro'yxat
</h2>
</div>
);
}Konsolda:
fokus: Ro'yxat
URL hash: ""Ekranda:
Ro'yxatga o'tish
Logo · Menyu · Qidiruv · Filtrlar
Ro'yxatIkki nozik joy:
tabIndex={-1}sarlavhada. Sarlavha odatda fokuslanmaydi.-1— "Tab bilan emas, lekin.focus()bilan mumkin". Endi keyingi Tab — ro'yxatdagi birinchi element.preventDefault. Oddiy#royxathavolasi URL'ga#royxatyozardi — brauzer tarixiga yangi qadam. SPA'da URL boshqa ma'noga ega bo'lishi mumkin (useSyncExternalStore: URL), shuning uchun fokusni qo'lda qo'yamiz va URL'ga tegmaymiz.
4.2 SPA'da "sahifa" almashganda
Oddiy saytda havola bosilsa — yangi sahifa yuklanadi, fokus boshiga qaytadi, ekran o'quvchi yangi sarlavhani o'qiydi. SPA'da (React Router) faqat kontent almashadi — fokus eski joyida (ko'pincha o'chirilgan havolada → <body>), Malika esa hech narsa eshitmaydi. Keng tarqalgan yechim — yangi "sahifa" chizilgach, fokusni uning <h1> iga qo'yish:
import { useEffect, useRef, useState } from "react";
function Page({ title }) {
const ref = useRef(null);
useEffect(() => {
ref.current.focus();
console.log("sahifa:", document.activeElement.textContent);
}, [title]);
return (
<main>
<h1 ref={ref} tabIndex={-1}>
{title}
</h1>
</main>
);
}
export default function App() {
const [page, setPage] = useState("Bosh sahifa");
return (
<div>
<nav>
<button onClick={() => setPage("Menyu")}>Menyu</button>{" "}
<button onClick={() => setPage("Buyurtmalar")}>
Buyurtmalar
</button>
</nav>
<Page title={page} />
</div>
);
}Konsolda:
sahifa: Bosh sahifa
sahifa: Menyu
sahifa: BuyurtmalarEkranda:
Menyu Buyurtmalar
BuyurtmalarDiqqat: birinchi qator — ochilishdagi fokus. Haqiqiy ilovada sahifa birinchi marta yuklanganda fokusni o'g'irlamang: brauzer o'zi boshidan boshlaydi, Malika esa sahifani boshidan o'qiyotgan bo'lishi mumkin. Odatda "birinchi render emas" sharti qo'shiladi. vazifalar ning auth qadamida aynan shunday qaror bor: fokus faqat shu tabda hozir kirilganda ko'chadi, sessiya bilan ochilganda — yo'q (React'da auth holati). React Router va Next.js'da bu xulq qisman o'rnatilgan — ularni o'rganganda qaytamiz.
5. aria-live: fokus siljimaydigan xabarlar
Ba'zi o'zgarishlar fokussiz bo'lishi kerak: "Vazifa qo'shildi", "Internet yo'q", "3 ta natija topildi". Fokusni xabarga ko'chirish — Boburni ishidan chalg'itadi. Malika esa xabarni baribir eshitishi kerak. Buning uchun — jonli hudud (live region): role="status" (yoki aria-live="polite") li element. Ichidagi matn o'zgarsa, ekran o'quvchi uni joriy gapdan keyin o'qiydi.
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
function add() {
const next = count + 1;
setCount(next);
setMessage(`Osh savatga qo'shildi. Jami: ${next}`);
}
return (
<div>
<button onClick={add}>Savatga</button>
<p role="status">{message}</p>
</div>
);
}Ekranda:
Savatga
Osh savatga qo'shildi. Jami: 2Uch qoida:
| Qoida | Nega |
|---|---|
| hudud sahifada oldindan bor, faqat matni o'zgaradi | yangi qo'shilgan hudud ko'pincha o'qilmaydi |
odatda — role="status" (polite) |
joriy gapni bo'lmaydi |
role="alert" (assertive) — faqat shoshilinch |
hamma narsani to'xtatib o'qiydi |
Bitta tuzoq: bir xil matn qayta yozilsa (ikki marta "qo'shildi"), ba'zi ekran o'quvchilar o'zgarishni sezmaydi. Shuning uchun yuqorida matnda son bor. vazifalar da beshta jonli hudud bor (oldingi dars) — e'lon, aloqa, qidiruv, statistika holati, forma xatosi. E'lon esa portal orqali <body> da turadi (Portallar).
6. Vazifalar qadami: «Ro'yxatga o'tish» va Bobur ssenariysi
6.1 Skip link
feat/a11y-otish ga bitta kod o'zgarishi. App.tsx da sarlavhadan keyin, kanondan (izoh qatorlari telefon uchun qayta bo'lingan):
{/* Skip link (18/#40, WCAG 2.4.1): klaviatura bilan
ishlovchi sahifa boshidan ro'yxatga bir bosishda o'tadi
(formadagi 8 ta to'xtash o'rniga). Fokusdagina ko'rinadi.
Fokus — sarlavhaga (tabIndex -1): keyingi Tab — birinchi
vazifa. preventDefault: #royxat URL'ga yozilmasin (tarix,
popstate — 17/#38) */}
<a
href="#royxat"
className="otish"
onClick={(event) => {
event.preventDefault();
sarlavhaRef.current?.focus();
}}
>
Ro'yxatga o'tish
</a>sarlavhaRef yangi emas — <h2 ref={sarlavhaRef} tabIndex={-1}>Ro'yxat</h2> 17-qismdan beri bor: vazifa o'chirilib, qo'shni qator qolmasa, fokus shu sarlavhaga tushadi. Ya'ni "fokusni yo'qotmaslik" qoidasi kanonda allaqachon ishlardi. CSS — vazifalar.css oxirida (izoh ikki qatorga bo'lingan):
/* 18/#40: skip link — fokusda paydo bo'ladi
(oqimdan tashqarida: CLS yo'q) */
.otish {
position: absolute;
top: var(--joy-2);
left: var(--joy-2);
z-index: 3;
padding: var(--joy-2) var(--joy-3);
border-radius: var(--burchak);
background: var(--rang-asosiy);
color: var(--rang-asosiy-ustida);
transform: translateY(-200%);
}
.otish:focus { transform: none; }position: absolute — havola sahifa oqimidan tashqarida, paydo bo'lganda hech narsani surmaydi (CLS 0, Portallar darsidagi o'lchov kabi). transform: translateY(-200%) — ekrandan tepaga yashirin, fokusda — joyiga. display: none bo'lmaydi: unda havolaga fokus umuman tushmasdi.
6.2 Bobur — haqiqiy ssenariy
Kanon e2e'da Bobur — sichqonchasiz, faqat keyboard.press bilan ishlaydigan ssenariy. Haqiqiy natija (natija-18-40.txt, build qilingan ilova, CSP bilan):
✅ Bobur: birinchi Tab — skip link, ko'rinadi: ["a|Ro'yxatga o'tish","none"]
✅ Bobur: Enter — fokus ro'yxat sarlavhasida, URL o'zgarmadi: ["h2|Ro'yxat",""]
✅ Bobur: Tab — birinchi vazifa katagi: "vazifa-1"
✅ Bobur: Space — belgilandi, qator pastga ko'chdi, fokus o'sha katakda: [["vazifa-1",true],["2","1","3"]]
✅ Bobur: Tab, Enter — tahrir maydoni fokusda, matn belgilangan: ["tahrir-maydon",0,9]
✅ Bobur: Enter — saqlandi, fokus «Tahrirlash» tugmasida: ["button|Tahrirlash: Non va sut olish","Non va sut olish"]
✅ Bobur: «O'chirish» — dialog, fokus «Bekor qilish»da: "button|Bekor qilish"
✅ Bobur: Esc — dialog yopildi, fokus «O'chirish»ga qaytdi: "button|O'chirish: Non va sut olish"Har qator — darsning bir bo'limi:
- 1–2-qatorlar — skip link: birinchi Tab'da ko'rinadi (
transform: none), Enter — sarlavhaga, URL bo'sh. - 4-qator — eng qiziq. Katak belgilanganda vazifa ro'yxatda pastga ko'chdi (
["2","1","3"]— 1-vazifa birinchi o'rindan ikkinchiga tushdi). React<li>ni DOM'da ko'chirdi va fokus tushib qolishi kerak edi. Lekin fokus o'sha katakda —fokusniRejala+useLayoutEffectuni qaytardi (useOptimistic qadamida qo'shilgan). - 5–6-qatorlar — tahrir: fokus maydonga, matn belgilangan (0–9 belgilar). Saqlangach — fokus «Tahrirlash» tugmasiga qaytdi, maydon o'chganiga qaramay.
- 7–8-qatorlar — native
<dialog>: «Bekor qilish» da ochildi, Esc bilan yopildi, fokus ochgan tugmaga qaytdi — «Modal oyna» bo'limidagi uch qoidaning hammasi.
6.3 Commit
git add -A
git commitCommit (kanon): feat: «Ro'yxatga o'tish» havolasi; a11y — axe 0, klaviatura ssenariysi. Tekshiruvlar (haqiqiy):
- axe-core — 6 holatda 0 buzilish; har holatda tekshirilgan qoidalar soni 3 taga oshdi (asosiy ko'rinishda 36 → 39,
40-axe-keyin.txt): yangi havola bilan bog'liq qoidalar endi qo'llanadi. npm test— node 170, Vitest 45; lint, format, tip, knip — toza.- Brauzer tekshiruvi — 159/159, e2e-18 — 63/63 (Bobur va Malika qo'shildi).
- Diff: 2 fayl, +28.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| element o'chdi — Tab sahifa boshidan | fokus <body> ga tushdi |
qo'shni element yoki sarlavhaga .focus() |
| modal ochiq, Tab orqada yuradi | trap yo'q | <dialog> + showModal() yoki inert |
| modal yopildi, fokus yo'qoldi | ochgan element eslanmagan | ref'da saqlab, .focus() |
sarlavhaga .focus() ishlamaydi |
tabIndex yo'q |
tabIndex={-1} |
| skip link hech ko'rinmaydi | display: none |
transform bilan yashirish, :focus da ko'rsatish |
| 10 ta filtr — 10 ta Tab | har tugma Tab to'xtashi | roving tabindex |
| xabar eshitilmadi | jonli hudud xabar bilan birga qo'shildi | hudud oldindan bor, faqat matni o'zgaradi |
| sahifa ochilishi bilan fokus sakraydi | birinchi render'da .focus() |
faqat foydalanuvchi harakatidan keyin |
8. Mashqlar
Mashqlar kurs/mashqlar/18/40-fokus/ da.
1-mashq (oson): qaysi texnika?
Har holat uchun: skip link, roving tabindex, aria-live yoki fokusni qaytarish.
- Hafta kunlari tanlovi (7 tugma) — Tab bilan bitta to'xtash bo'lsin.
- "Buyurtma yuborildi" — fokus siljimasin, lekin Malika eshitsin.
- Rasm galereyasi yopildi — fokus ochgan rasmga.
- Sahifa boshidagi 15 ta havoladan sakrab o'tish.
Yechim
- roving tabindex — guruh bitta Tab to'xtash, ichida ← →.
- aria-live —
role="status"hudud, oldindan sahifada. - fokusni qaytarish — ochgan elementni ref'da saqlab, yopilganda
.focus(). - skip link — birinchi Tab'da ko'rinadigan havola, asosiy qismga.
2-mashq (o'rta): o'chirilgan qatordan keyin fokus
Taomlar ro'yxati, har qatorda «O'chirish». O'chirilgandan keyin fokus keyingi qatorning «O'chirish» tugmasiga tushsin; keyingi bo'lmasa — oldingisiga; ro'yxat bo'shasa — <h2> sarlavhaga (tabIndex={-1}). Konsolga fokus qayerda ekanini chiqaring. Ishora: tugmalar — Map ref (useRef II); fokus — DOM yangilangandan keyin (useEffect yoki useLayoutEffect useLayoutEffect).
Yechim
import { useLayoutEffect, useRef, useState } from "react";
export default function App() {
const [dishes, setDishes] = useState(["Osh", "Manti", "Lag'mon"]);
const buttons = useRef(new Map());
const headingRef = useRef(null);
const [focusPlan, setFocusPlan] = useState(null);
useLayoutEffect(() => {
if (focusPlan === null) return;
const target =
buttons.current.get(focusPlan) ?? headingRef.current;
target.focus();
console.log("fokus:", document.activeElement.textContent);
}, [focusPlan, dishes]);
function remove(dish) {
const i = dishes.indexOf(dish);
const next = dishes[i + 1] ?? dishes[i - 1] ?? "__sarlavha";
setDishes(dishes.filter((d) => d !== dish));
setFocusPlan(next);
}
return (
<section>
<h2 ref={headingRef} tabIndex={-1}>
Menyu
</h2>
<ul>
{dishes.map((d) => (
<li key={d}>
{d}{" "}
<button
ref={(el) => {
if (el) buttons.current.set(d, el);
else buttons.current.delete(d);
}}
onClick={() => remove(d)}
>
O'chirish: {d}
</button>
</li>
))}
</ul>
</section>
);
}Konsolda:
fokus: O'chirish: Lag'mon
fokus: O'chirish: Osh
fokus: MenyuEkranda:
MenyuFokus rejasi — state: o'chirish paytida element hali DOM'da, .focus() ni render'dan keyin qilish kerak. useLayoutEffect — bo'yashdan oldin, foydalanuvchi "fokussiz" holatni ko'rmaydi. Ref callback null bilan chaqirilganda (element o'chdi) — Map dan o'chiramiz. Tugma matnida taom nomi — Malika qaysi qatorni o'chirayotganini eshitadi. vazifalar dagi fokusla(id, amal, zaxiraId) — xuddi shu g'oya.
3-mashq (qiyin): radio guruh — roving tabindex
Yetkazish usuli: «Olib ketish», «Yetkazish», «Zalda» — role="radiogroup" va har biri role="radio" li <button>. Talablar:
- Faqat tanlangan element
tabIndex={0}, qolganlari-1. aria-checked— tanlangandatrue.- ← → — tanlov ham, fokus ham ko'chadi (radio guruhda strelka tanlaydi).
- Bosish ham tanlaydi; konsolga
tanlandi:va nom.
Ishora: «Roving tabindex» bo'limidagi nextRoving; role="radiogroup" ga aria-label.
Yechim
import { useRef, useState } from "react";
const OPTIONS = ["Olib ketish", "Yetkazish", "Zalda"];
function nextRoving(key, index, count) {
if (key === "ArrowRight" || key === "ArrowDown") {
return (index + 1) % count;
}
if (key === "ArrowLeft" || key === "ArrowUp") {
return (index - 1 + count) % count;
}
return index;
}
export default function App() {
const [selected, setSelected] = useState(0);
const refs = useRef([]);
function select(i) {
setSelected(i);
refs.current[i].focus();
console.log("tanlandi:", OPTIONS[i]);
}
return (
<div
role="radiogroup"
aria-label="Yetkazish usuli"
onKeyDown={(e) => {
const next = nextRoving(e.key, selected, OPTIONS.length);
if (next !== selected) {
e.preventDefault();
select(next);
}
}}
>
{OPTIONS.map((name, i) => (
<button
key={name}
ref={(el) => {
refs.current[i] = el;
}}
role="radio"
aria-checked={i === selected}
tabIndex={i === selected ? 0 : -1}
onClick={() => select(i)}
>
{name}
</button>
))}
</div>
);
}Konsolda:
tanlandi: ZaldaEkranda:
Olib ketishYetkazishZaldaToolbar'dan farqi: radio guruhda strelka tanlovni ham o'zgartiradi (APG naqshi shunday). Tab bilan guruhdan chiqib, qayta kirsangiz — fokus tanlangan elementga keladi, birinchisiga emas. Amalda native <input type="radio"> guruhi bularning hammasini bepul beradi — o'z radio'ngiz faqat ko'rinish talab qilganda kerak.
4-mashq: Vazifalar qadami
Skip link'ni qo'shing (kanon parchasi va CSS). npm run build va npm run preview, keyin Boburning 8 qadamini o'zingiz qo'lda takrorlang. Har qadamda fokus qayerda ekanini DevTools konsolida tekshiring: document.activeElement. Commit qiling.
Yechim
- Birinchi Tab — «Ro'yxatga o'tish» ko'rinadi. Enter — fokus «Ro'yxat» sarlavhasida, manzil qatorida
#royxatyo'q. - Katak Space bilan belgilanganda qator pastga ketadi,
document.activeElement— o'sha katak. - O'chirish dialogi: Tab faqat ikki tugma orasida, Esc — fokus «O'chirish» ga.
npm run check— toza; axe — 0. Commit —feat: «Ro'yxatga o'tish» havolasi; a11y — axe 0, klaviatura ssenariysi. PR'ni birlashtiring.
9. Real ishda
- Dizayn tizimlari — Radix, React Aria, Headless UI fokusni ichkarida boshqaradi:
Dialog,Menu,Tabs. Ularni tanlashning asosiy sababi — aynan shu (Mantine, Chakra va React Aria). O'zingiz yozsangiz — APG naqshi bo'yicha. - Router'lar — sahifa almashganda fokus va e'lon: Next.js "route announcer"ga ega, React Router'da — o'zingiz.
- e2e testlar — Playwright'da
page.keyboard.press("Tab")vaexpect(page.locator(":focus"))—vazifalardagi Bobur ssenariysi shunday (Playwright I). - Intervyu savollari: "modal oyna uchun a11y talablari?", "roving tabindex nima?", "
tabIndex={-1}va0farqi?", "inertnima qiladi?".
Xulosa
- React fokusni boshqarmaydi: element o'chsa — fokus
<body>ga. Qo'shni element yoki sarlavhaga.focus()— DOM yangilangandan keyin. - Modal: ochilganda — ichkariga (xavfsiz tugma), ochiq paytda — trap, yopilganda — ochgan elementga. Native
<dialog>+showModal()ikkitasini o'zi qiladi;inert— orqa fonni o'chiradi. - Roving tabindex: guruh — bitta Tab to'xtash (
tabIndex0/-1), ichida strelkalar. - Skip link — birinchi Tab, sarlavha
tabIndex={-1}; SPA'da sahifa almashganda —<h1>ga fokus (birinchi render'da emas). - Fokussiz xabar — oldindan turgan
role="status"hudud. vazifalar: «Ro'yxatga o'tish», Bobur ssenariysi 8/8, e2e-18 63/63, commit.
Keyingi dars: Frontend xavfsizligi React'da — React XSS'dan qayerda himoya qiladi va qayerda yo'q.
Manbalar
- W3C: WAI-ARIA APG — Dialog (Modal), Toolbar, Radio Group, Menu Button naqshlari — w3.org/WAI/ARIA/apg/patterns
- WCAG 2.2: 2.4.1 Bypass Blocks, 2.4.3 Focus Order — w3.org/TR/WCAG22
- MDN:
HTMLDialogElement.showModal(),inert— developer.mozilla.org - React 19: "
inert— boolean atribut" — react.dev/reference/react-dom/components/common (2026-10 holatiga)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!