Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. cmdk: qidiruvli ro'yxat
- 2.1 Birinchi buyruqlar oynasi
- 2.2 Klaviatura
- 2.3 Fuzzy search o'zbekcha matnda
- 3. Ctrl+K: yorliqni ulash
- 3.1 key yoki code?
- 3.2 Platformani aniqlash
- 3.3 O'quv modeli: Ctrl+K bilan ochiladigan buyruqlar oynasi
- 4. Bitta harfli yorliqlar va maydonlar
- 4.1 Muammo
- 4.2 react-hotkeys-hook
- 5. A11y: yorliqni e'lon qilish
- 6. Vazifalar qadami: Ctrl+K buyruqlar oynasi (holat 11)
- 6.1 Ikki komponent
- 6.2 BuyruqlarRoyxati.tsx — cmdk qismi
- 6.3 BuyruqlarOynasi.tsx — tugma, yorliq, dialog va lazy
- 6.4 Narx va tekshiruvlar
- 6.5 Commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): sahifa va vazifalar guruhlari
- 3-mashq (qiyin): tartibdan mustaqil yorliq hooki
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
Command palette React'da: cmdk, Ctrl+K va klaviatura yorliqlari
Qisqacha: Buyruqlar oynasi (command palette) — Ctrl+K (Mac'da K) bilan ochiladigan oyna: yozasiz, ro'yxat qisqaradi, ↑↓ va Enter bilan sahifaga yoki amalga o'tasiz. React'da uning ichini cmdk beradi: qidiruv maydoni, filtrlangan ro'yxat,
combobox+listboxa11y. Yorliqni o'zingiz ulaysiz:keydowndactrlKey/metaKey,preventDefault(), bitta harfli yorliqlar esa maydon ichida o'chiriladi.vazifalarda oyna — native<dialog>, cmdk esa birinchi Ctrl+K da yuklanadi (6.3 KB chunk).
Bu darsda
- cmdk bilan qidiruvli buyruqlar ro'yxatini yasaysiz va klaviatura yo'lini jonli tekshirasiz.
- cmdk'ning standart "fuzzy" qidiruvi o'zbekcha matnda qayerda adashishini ko'rasiz va o'z filtringizni yozasiz.
- Ctrl+K va K ni platformaga qarab ulaysiz,
keyvacodeni to'g'ri tanlaysiz. - Bitta harfli yorliqlarni maydon ichida o'chirasiz, react-hotkeys-hook'ni bilasiz.
- Oynaning a11y'sini (
aria-keyshortcuts, fokus,<kbd>) to'g'ri qilasiz. vazifalarga buyruqlar oynasini qo'shasiz — cmdk birinchi ochilishda yuklanadi (holat 11, commit).
Oldin bilishingiz kerak: Klaviatura hodisalari va tezkor tugmalar, Modal, drawer, popover va tooltip, lazy va code splitting, Matn algoritmlari, Fokus boshqaruvi.
1. Nega bu kerak?
vazifalar o'sdi: ro'yxat, statistika, sozlamalar, "katta ishni bo'lish", har vazifaning batafsil sahifasi. Sardor kun bo'yi sichqoncha bilan menyuni ochib, sahifa qidiradi. Bobur (faqat klaviatura) uchun esa har sahifaga Tab bilan yetib borish — o'nlab bosish.
Klaviatura hodisalari darsida Ctrl+K bilan qidiruv maydoniga o'tishni yozgan edik va "command palette" ni tilga olgan edik — GitHub, VS Code, Slack, Notion'dagi "buyruqlar oynasi". Bugun shuni React'da quramiz: Ctrl+K → oyna → "stat" deb yozish → Enter → statistika sahifasi. To'rt harakat, sichqonchasiz.
Hayotiy o'xshatish: katta bozordagi ma'lumot byurosi. Har rastani aylanib qidirish o'rniga, byuroga "ko'k choy" deysiz — sizga aniq joyni ko'rsatishadi.
2. cmdk: qidiruvli ro'yxat
2.1 Birinchi buyruqlar oynasi
cmdk (1.1.1) — buyruqlar oynasining "ichi": qidiruv maydoni, ro'yxat, filtrlash, tanlangan band va klaviatura. shadcn'dagi Command komponenti aynan shu (shadcn/ui II). Natija oynasida cmdk bor.
import { Command } from "cmdk";
export default function App() {
return (
<Command label="Buyruqlar">
<Command.Input placeholder="Qidirish…" />
<Command.List>
<Command.Empty>Hech narsa topilmadi.</Command.Empty>
<Command.Item>Non olish</Command.Item>
<Command.Item>Kitob o'qish</Command.Item>
<Command.Item>Sozlamalar</Command.Item>
</Command.List>
</Command>
);
}Ekranda:
Non olishQismlar — Compound komponentlar naqshida:
Command— ildiz: holat (qidiruv matni, tanlangan band) valabel— ekran o'quvchi uchun nom. U yashirin<label>bo'lib chiziladi: ko'zga ko'rinmaydi (shuning uchun «Ekranda» yo'q), lekin ekran o'quvchi maydonni «Buyruqlar» deb e'lon qiladi.Command.Input— qidiruv maydoni.Command.List— ro'yxat;Command.Item— band;Command.Empty— hech narsa topilmasa.
«nl» yozdik — «Non olish» qoldi. "nl" so'z emas-ku? Bu cmdk'ning fuzzy search (taxminiy qidiruv) i: harflar ketma-ket emas, lekin shu tartibda uchrasa ham mos deb hisoblanadi — Non olish. Bu command-score kutubxonasi (cmdk turlaridagi izoh: "By default, uses the command-score library").
Testlar uchun bir eslatma: cmdk brauzerning ResizeObserver (ro'yxat balandligini kuzatadi) va scrollIntoView (tanlangan bandni ko'rinadigan joyga suradi) API'laridan foydalanadi. Brauzerda ikkalasi bor. Vitest ishlatadigan jsdom'da esa yo'q — u yerda test sozlash faylida bo'sh o'rinbosar yoziladi.
2.2 Klaviatura
Endi Bobur yo'li — faqat klaviatura. «Ishga tushir» dan keyin maydonni bosing, ↓ ni ikki marta, keyin Enter ni bosing. Guruh va tanlov:
export default function App() {
const choose = (value) => console.log("tanlandi:", value);
return (
<Command label="Buyruqlar" loop>
<Command.Input placeholder="Qidirish…" />
<Command.List>
<Command.Group heading="Sahifalar">
<Command.Item onSelect={choose}>Ro'yxat</Command.Item>
<Command.Item onSelect={choose}>Statistika</Command.Item>
<Command.Item onSelect={choose}>Sozlamalar</Command.Item>
</Command.Group>
</Command.List>
</Command>
);
}Konsolda:
tanlandi: SozlamalarQadamma-qadam:
| Qadam | Fokus | Tanlangan band | Natija |
|---|---|---|---|
| maydonni bosish | maydon | Ro'yxat | — |
| ↓ | maydon | Statistika | — |
| ↓ | maydon | Sozlamalar | — |
| Enter | maydon | Sozlamalar | tanlandi: Sozlamalar |
Ochilganda birinchi band tanlangan. ↓ ↓ — uchinchisi, Enter — onSelect. Fokus esa doim maydonda qoladi: odam yozishda davom etishi mumkin. Qaysi band "tanlangan" ekanini ekran o'quvchiga aria-activedescendant aytadi — maydon fokusda, lekin "faol" band ro'yxatdagi element. Bu WAI-ARIA'ning combobox naqshi: maydon role="combobox", ro'yxat listbox, bandlar option.
Command.Group heading— guruh sarlavhasi («Sahifalar», «Vazifalar»).loop— oxiridan ↓ — boshiga.onSelect(value)— Enter yoki bosish.value— band matni (yokivalueprop'i).
cmdk'da yana bir narsa standart yoqilgan: vimBindings — Ctrl+N/J (pastga), Ctrl+P/K (tepaga). Ilovangizning o'z Ctrl+K si bo'lsa, ikkalasi bir tugmada — keraksiz bo'lsa vimBindings={false}.
2.3 Fuzzy search o'zbekcha matnda
Jasur aka «o‘qish» deb yozdi — telefonidagi klaviatura apostrofni ‘ (U+2018) bilan qo'ydi:
export default function App() {
return (
<Command label="Buyruqlar">
<Command.Input placeholder="Qidirish…" />
<Command.List>
<Command.Empty>Hech narsa topilmadi.</Command.Empty>
<Command.Item>Non olish</Command.Item>
<Command.Item>Kitob o'qish</Command.Item>
</Command.List>
</Command>
);
}Ekranda:
Hech narsa topilmadi.«Kitob o'qish» bor, lekin topilmadi: ‘ va ' — boshqa-boshqa belgilar. O'zbek tilida apostrofning kamida beshta varianti yuradi (', ‘, ’, ʻ, ʼ) — Matn algoritmlari darsida normalla funksiyasi aynan shuning uchun yozilgan.
Ikkinchi muammo — teskarisi: «nl» uchun «Non olish» chiqdi. Qisqa ro'yxatda bu yordam, lekin 300 vazifada "nl" deb yozganingizda o'nlab kutilmagan natija chiqadi.
Davosi — o'z filtringiz. cmdk'da filter prop'i bor: (value, search, keywords) => son — 1 — mos, 0 — yashirilsin. Kanon (BuyruqlarRoyxati.tsx) shunday qilgan. Bu yerda normalla ning soddalashgan nusxasi bilan:
// Soddalashgan normalla: kichik harf + apostroflar bir xil
const normalize = (text) =>
text.toLowerCase().replace(/[‘’ʻʼ`]/g, "'");
function matches(value, search, keywords = []) {
const text = normalize([value, ...keywords].join(" "));
return text.includes(normalize(search)) ? 1 : 0;
}
export default function App() {
return (
<Command label="Buyruqlar" filter={matches}>
<Command.Input placeholder="Qidirish…" />
<Command.List>
<Command.Empty>Hech narsa topilmadi.</Command.Empty>
<Command.Item>Non olish</Command.Item>
<Command.Item>Kitob o'qish</Command.Item>
</Command.List>
</Command>
);
}Ekranda:
Kitob o'qishEndi includes — oddiy "ichida bormi?" qidiruvi: «nl» endi hech narsa topmaydi, «o‘qish» esa topadi. Fuzzy'dan voz kechdik — bu ataylab: vazifa va sahifa nomlari qisqa, aniq qidiruv tushunarliroq. Kanon izohi: "cmdk'ning standart "fuzzy" bahosi o'zbekcha apostrofni bilmaydi va "nl" ni "Non olish" ga mos deydi".
Tekshirib ko'ring: Kanonda har band
value={vazifa:${v.id}}vakeywords={[v.matn]}bilan yozilgan. Nega matnning o'zinivalueqilmaydi?
Javob
value — bandning noyob kaliti: cmdk tanlovni shu bilan eslab qoladi. Ikki vazifa bir xil matnli bo'lsa («Non olish» ikki marta), ikkalasining value si bir xil bo'lib qolardi va tanlov chalkashardi. vazifa:3 — doim noyob. Qidiruv esa keywords bo'yicha: kanonning mosmi filtri faqat keywords ni normallab tekshiradi. Bonus: "sahifa:" va "vazifa:" prefikslari bir xil nomli sahifa va vazifani ham ajratadi.
3. Ctrl+K: yorliqni ulash
3.1 key yoki code?
Klaviatura hodisalari darsidagi jadvalni eslang: Ctrl + harf uchun code, chunki rus yoki kirill tartibida K tugmasining key i — «л», code esa doim KeyK. Tekshiruvni sof funksiya qilib, uch holatda sinaymiz:
// mac — Mac'dami (u yerda Cmd), aks holda Ctrl
function isPaletteShortcut(event, mac) {
const mod = Boolean(mac ? event.metaKey : event.ctrlKey);
const extra = event.altKey || event.shiftKey;
return mod && event.code === "KeyK" && !extra;
}
const latin = { ctrlKey: true, key: "k", code: "KeyK" };
const russian = { ctrlKey: true, key: "л", code: "KeyK" };
const macCmd = { metaKey: true, key: "k", code: "KeyK" };
console.log("lotin, Windows:", isPaletteShortcut(latin, false));
console.log("rus tartibi:", isPaletteShortcut(russian, false));
console.log("Mac, ⌘K:", isPaletteShortcut(macCmd, true));
console.log("Mac, Ctrl+K:", isPaletteShortcut(latin, true));
console.log("key bilan, rus:", russian.key.toLowerCase() === "k");Konsolda:
lotin, Windows: true
rus tartibi: true
Mac, ⌘K: true
Mac, Ctrl+K: false
key bilan, rus: falsecodebilan — tartibdan qat'i nazar ishlaydi.- Mac'da Ctrl+K —
false: Mac'da odatiy modifikator — . Ikkalasini qabul qilish ham mumkin (ctrlKey || metaKey, Klaviatura hodisalari darsidagi kabi) — lekin Mac'da Ctrl+K matn maydonida "qator oxirigacha o'chirish" vazifasini bajaradi, shuning uchun platformaga qarab ajratish yaxshiroq. !altKey && !shiftKey— Ctrl+Shift+K boshqa yorliq (masalan, Firefox'da konsol). Aniq kombinatsiya.- Oxirgi qator —
keybilan tekshiruv rus tartibida ishlamaydi.
Diqqat — biz ham shu xatoga tushgan edik.
vazifalardagiBuyruqlarOynasi.tsxning birinchi varianti yorliqnievent.key.toLowerCase() === "k"bilan tekshirgan — yuqoridagi oxirgi qatordagi usul. e2e testlari lotin tartibida yurardi, shuning uchun xato ko'rinmadi. Keyin e2e-20 ga yangi tekshiruv qo'shdik: brauzerga Ctrl + «л» (code—KeyK) yuboriladi. Eski kod[false,false]berdi — oyna ochilmadi.event.code === "KeyK"ga o'tilgach, test o'tdi:[true,false](ochildi, qayta bosish yopdi).Saboq: test faqat siz o'ylagan holatni tekshiradi. Rus tartibidagi foydalanuvchi testda bo'lmasa, uning xatosi ham ko'rinmaydi. 3-mashqda shu tuzatishni o'zingiz takrorlaysiz.
3.2 Platformani aniqlash
Kanon:
// Mac'da Cmd, qolganlarida Ctrl. navigator.platform eskirgan, lekin
// hamma brauzerda bor (userAgentData — faqat Chromium'da)
const MAC = /Mac|iPhone|iPad/.test(navigator.platform);
const YORLIQ = MAC ? "⌘K" : "Ctrl+K";navigator.platform — "eskirgan" (deprecated) deb belgilangan, lekin hamma brauzerda ishlaydi. Yangi navigator.userAgentData.platform faqat Chromium'da bor. Shuning uchun kanon eskisini tanlagan va izohda sababini yozgan — bu to'g'ri odat.
YORLIQ — foydalanuvchiga ko'rsatiladigan matn (tooltip'da, oldingi dars). Mac foydalanuvchisi «Ctrl+K» ni ko'rsa, adashadi.
3.3 O'quv modeli: Ctrl+K bilan ochiladigan buyruqlar oynasi
Endi hammasini birga. Telefonda Ctrl tugmasi yo'q, shuning uchun «Ctrl+K (sinov)» tugmasi window ga xuddi shunday keydown hodisasini yuboradi — haqiqiy klaviatura o'rnida "robot":
import { useEffect, useRef, useState } from "react";
function pressCtrlK() {
const init = { key: "k", code: "KeyK", ctrlKey: true };
window.dispatchEvent(new KeyboardEvent("keydown", init));
}
const go = (path) => () => console.log("→", path);
export default function App() {
const [open, setOpen] = useState(false);
const input = useRef(null);
useEffect(() => {
function onKeyDown(event) {
if (isPaletteShortcut(event, false)) {
event.preventDefault(); // brauzerning o'z Ctrl+K si
setOpen((o) => !o);
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
useEffect(() => {
if (open) input.current?.focus();
}, [open]);
return (
<div>
<button onClick={pressCtrlK}>Ctrl+K (sinov)</button>
{open && (
<Command label="Buyruqlar" filter={matches}>
<Command.Input ref={input} placeholder="Qidirish…" />
<Command.List>
<Command.Item onSelect={go("/statistika")}>
Statistika
</Command.Item>
<Command.Item onSelect={go("/sozlamalar")}>
Sozlamalar
</Command.Item>
</Command.List>
</Command>
)}
</div>
);
}
function isPaletteShortcut(event, mac) {
const mod = Boolean(mac ? event.metaKey : event.ctrlKey);
const extra = event.altKey || event.shiftKey;
return mod && event.code === "KeyK" && !extra;
}Ekranda:
Ctrl+K (sinov)
Statistika«Ctrl+K (sinov)» ni bosing va «stat» deb yozing. Oyna ochildi, fokus maydonga o'tdi (shuning uchun yozish darhol maydonga tushdi), «stat» bo'yicha faqat «Statistika» qoldi. Uch muhim joy:
preventDefault()— Chrome'da Ctrl+K manzil satrida qidiruvni ochadi. Busiz ikkalasi birga ishlardi.windowdagi tinglovchi — yorliq sahifaning qayerida bo'lmasin ishlaydi, maydon ichida ham. Ctrl+K matn yozmaydi, shuning uchun maydon ichida ham xavfsiz. Effekt cleanup bilan (useEffect).- Toggle — ochiq oynada Ctrl+K uni yopadi. Ko'p ilovalarda shunday.
matches — oldingi blokdagi filtr (davomi — bitta modul). Haqiqiy ilovada oyna <dialog> ichida bo'ladi — «Vazifalar qadami» bo'limida.
4. Bitta harfli yorliqlar va maydonlar
4.1 Muammo
Gmail'da c — yangi xat, / — qidiruv. Sardor vazifalar ga ham qo'shmoqchi: n — yangi vazifa. Lekin Jasur aka vazifa nomiga «Non olish» deb yozganda… birinchi harf n — yangi vazifa oynasi ochildi!
Qoida: modifikatorsiz yorliqlar maydon ichida ishlamasin. Ctrl+K — mumkin (u yozmaydi), n, /, ?, j, k — yo'q.
Tekshirish kerak bo'lgan joylar: <input>, <textarea>, <select> va contenteditable (boy matn muharrirlari). Yana bitta — event.isComposing: IME (masalan, xitoy yoki yapon yozuvi) bilan so'z terilayotganda tugmalar hali "harf" emas.
function isTypingTarget(target) {
if (target.isContentEditable) return true;
return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
}
function shouldHandle(event) {
if (event.isComposing) return false;
if (event.ctrlKey || event.metaKey || event.altKey) return true;
return !isTypingTarget(event.target);
}
const inInput = { key: "n", target: { tagName: "INPUT" } };
const onPage = { key: "n", target: { tagName: "BODY" } };
const input = { tagName: "INPUT" };
const ctrlInInput = { key: "k", ctrlKey: true, target: input };
const div = { tagName: "DIV", isContentEditable: true };
const editor = { key: "n", target: div };
console.log("n maydonda:", shouldHandle(inInput));
console.log("n sahifada:", shouldHandle(onPage));
console.log("Ctrl+K maydonda:", shouldHandle(ctrlInInput));
console.log("n muharrirda:", shouldHandle(editor));Konsolda:
n maydonda: false
n sahifada: true
Ctrl+K maydonda: true
n muharrirda: falseSinov uchun oddiy obyektlar ishlatdik — funksiya faqat key, target va modifikatorlarni o'qiydi. Bu sof funksiyani testlash oson (Toza funksiya g'oyasi).
4.2 react-hotkeys-hook
Ko'p yorliq bo'lsa, ularni bitta hook bilan ulash qulay. Eng ko'p ishlatiladigani — react-hotkeys-hook (5.3.3, natija oynasida yo'q):
import { useHotkeys } from "react-hotkeys-hook";
// "mod" — Mac'da ⌘, boshqalarda Ctrl
useHotkeys("mod+k", () => setOpen((o) => !o), {
enableOnFormTags: true, // maydon ichida ham (Ctrl+K yozmaydi)
preventDefault: true,
});
// Bitta harf — standart bo'yicha maydon ichida ishlamaydi
useHotkeys("n", () => yangiVazifa());Paketni o'rnatib, ichini ko'rdik (2026-10-17):
mod— kod ichida Mac tekshiruvi bilanmetaKeyyokictrlKeyga aylanadi (biz qo'lda yozganisPaletteShortcutning o'zi).enableOnFormTags— standart bo'yicha o'chiq: yorliqlar maydonlarda ishlamaydi («Muammo» bo'limidagi qoida).- Tugmani
codebo'yicha taniydi (ichidacodeni nomga aylantiradi);useKey: truebilankeyga o'tadi. Ya'ni standart xulq — tartibdan mustaqil.
vazifalar da bitta yorliq bor (Ctrl+K), shuning uchun kanon kutubxona qo'shmadi — 17-qismdan beri bor useOynaHodisasi hooki yetdi.
5. A11y: yorliqni e'lon qilish
Yorliq ko'rinmas narsa. Uni bilmagan odam uchun u yo'q. Uch qatlamda aytamiz:
- Tugma — oynani ochadigan tugma doim bor (sichqoncha, sensorli ekran va yorliqni bilmaganlar uchun). Kanonda: « Buyruqlar».
aria-keyshortcuts— ekran o'quvchiga:aria-keyshortcuts="Control+K"(Mac'da"Meta+K"). Qiymat — ARIA nomlari,+bilan.- Ko'rinadigan yozuv — tooltip «Tezkor ochish: Ctrl+K» (oldingi dars) yoki
<kbd>Ctrl</kbd>+<kbd>K</kbd>(W-18).
Oyna ichida:
- ochilganda fokus — qidiruv maydoniga;
- Esc — yopadi, fokus ochgan tugmaga qaytadi (Fokus boshqaruvi);
- Enter bilan sahifaga o'tilganda — fokus yangi sahifa sarlavhasiga (19-qismdagi
Qobiqqoidasi).
6. Vazifalar qadami: Ctrl+K buyruqlar oynasi (holat 11)
6.1 Ikki komponent
Oldingi darsda feat/buyruqlar branch'ini ochib, tooltip'ni qo'shgan edik. Endi oynaning o'zi:
npm i -E cmdk@1.1.1package.json ga yana bitta narsa — overrides:
"overrides": {
"@radix-ui/react-dialog": "1.2.0"
}Nega? cmdk 1.1.1 @radix-ui/react-dialog ga ^1.1.6 bilan bog'langan (u Command.Dialog uchun). ^ diapazoni o'rnatish kunidagi eng yangi versiyani tortardi — kanon yozganda bu 2 kunlik 1.3.1 edi. Kanon qoidasi — 7 kundan yangi reliz olinmaydi, shuning uchun overrides bilan qotirildi (Semver va lock fayl qoidalari).
Oyna ikki faylga bo'lingan:
BuyruqlarOynasi.tsx— doim yuklanadi: tugma, tooltip, Ctrl+K va<dialog>. cmdk'ni import qilmaydi.BuyruqlarRoyxati.tsx— oynaning ichi: cmdk, qidiruv, ro'yxat. Alohida chunk, birinchi ochilishda yuklanadi.
Nega bunday? Kanon birinchi variantda cmdk'ni to'g'ridan import qilgan edi: birinchi yuklash +9.0 KB. Kod ko'rigida bu qabul qilinmadi: cmdk birinchi Ctrl+K da yuklanishi kerak — lazy va code splitting darsidagi qoida o'z ilovamizda ham. Oynani hamma ham ochmaydi — nega hamma uni yuklab olishi kerak?
Birinchi ochilishdagi tartib:
sequenceDiagram
participant U as Foydalanuvchi
participant O as BuyruqlarOynasi
participant N as Tarmoq
U->>O: Ctrl+K (yoki tugma)
O->>O: showModal(), «Yuklanmoqda…»
O->>N: import("./BuyruqlarRoyxati")
N-->>O: cmdk chunk (6.3 KB)
O->>O: ro'yxat chiziladi, fokus maydonga
U->>O: «statis» + Enter
O->>U: /statistika, fokus sarlavhadaNimaga qarang: oyna chunk'ni kutmaydi — darhol ochiladi va role="status" li «Yuklanmoqda…» ni ko'rsatadi. Ikkinchi ochilishda chunk keshda: ro'yxat bir zumda.
6.2 BuyruqlarRoyxati.tsx — cmdk qismi
Kanon fayli (sarlavha izohi qisqartirilgan):
// BuyruqlarRoyxati.tsx — buyruqlar oynasining ichi (20/#11): cmdk.
// Alohida chunk: BuyruqlarOynasi uni birinchi Ctrl+K (yoki tugma) da
// lazy() bilan yuklaydi
import { useEffect, useRef, useState } from "react";
import { Command } from "cmdk";
import { normalla } from "@/qidiruv.ts";
import { useVazifalar } from "@/server-sinxron.ts";
const SAHIFALAR = [
{ yol: "/", nomi: "Ro'yxat" },
{ yol: "/statistika", nomi: "Statistika" },
{ yol: "/sozlamalar", nomi: "Sozlamalar" },
{ yol: "/yangi", nomi: "Katta ishni qadamlarga bo'lish" },
] as const;
// Qidiruv — o'zimizniki (14/#11 normalla: kichik harf, apostroflar
// ' ‘ ’ ʻ ʼ bir xil): cmdk'ning standart "fuzzy" bahosi
// o'zbekcha apostrofni bilmaydi va "nl" ni "Non olish" ga mos deydi
function mosmi(_qiymat: string, sorov: string, kalitlar?: string[]) {
return normalla((kalitlar ?? []).join(" ")).includes(normalla(sorov))
? 1
: 0;
}
type Props = {
bor: (yol: string) => void;
};
// default eksport — lazy() shuni kutadi
export default function BuyruqlarRoyxati({ bor }: Props) {
const [sorov, setSorov] = useState("");
const maydon = useRef<HTMLInputElement>(null);
const { vazifalar } = useVazifalar();
// Birinchi ochilishda chunk showModal'dan KEYIN keladi (oyna
// allaqachon ochiq, fokus <dialog> da) — maydon paydo bo'lganda
// fokusni o'zi oladi
useEffect(() => {
maydon.current?.focus();
}, []);
return (
<Command label="Buyruqlar" filter={mosmi} loop>
<Command.Input
ref={maydon}
value={sorov}
onValueChange={setSorov}
placeholder="Sahifa yoki vazifa nomi…"
className="mb-2 min-h-bosish w-full rounded-burchak border-2 border-ikkinchi bg-yuza px-2"
/>
<Command.List className="max-h-80 overflow-y-auto">
<Command.Empty className="px-3 py-2 text-ikkinchi">
Hech narsa topilmadi.
</Command.Empty>
<Command.Group heading="Sahifalar" className={GURUH}>
{SAHIFALAR.map((s) => (
<Command.Item
key={s.yol}
value={`sahifa:${s.yol}`}
keywords={[s.nomi]}
onSelect={() => bor(s.yol)}
className={BAND}
>
{s.nomi}
</Command.Item>
))}
</Command.Group>
{/* «Vazifalar» guruhi — xuddi shunday:
value={`vazifa:${v.id}`}, keywords={[v.matn]},
bor(`/vazifa/${v.id}`) */}
</Command.List>
</Command>
);
}BAND va GURUH — Tailwind sinflari (tanlangan band data-[selected=true]:bg-asosiy); qisqartirish uchun ko'rsatilmadi. default export — lazy() faqat shuni qabul qiladi (lazy). useVazifalar() — Query keshidan, so'rovsiz (TanStack Query).
6.3 BuyruqlarOynasi.tsx — tugma, yorliq, dialog va lazy
import { lazy, Suspense, useRef, useState } from "react";
import { flushSync } from "react-dom";
import { Search } from "lucide-react";
import { useNavigate } from "react-router";
import { useOynaHodisasi } from "@/hooklar/useOynaHodisasi.ts";
import { XatoChegarasi } from "@/komponentlar/XatoChegarasi.tsx";
import { Tugma } from "@/ui/Tugma.tsx";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/ui/tooltip.tsx";
const MAC = /Mac|iPhone|iPad/.test(navigator.platform);
const YORLIQ = MAC ? "⌘K" : "Ctrl+K";
// Modul Promise'i bitta (StatistikaPaneli.tsx dagidek, 18/#09):
// oldindan yuklash va lazy() ikkalasi shuni kutadi
let modul: Promise<typeof import("./BuyruqlarRoyxati.tsx")> | null =
null;
function modulniOl() {
modul ??= import("./BuyruqlarRoyxati.tsx");
return modul;
}
const BuyruqlarRoyxati = lazy(modulniOl);
// Sichqoncha tugma ustiga kelsa — bosishdan oldin yuklay
// boshlaymiz. Klaviatura fokusida EMAS: Tab bilan o'tib ketgan
// hamma yuklab olardi. Xato bu yerda jim — ochilganda chegara
// ko'rsatadi
function oldindanYukla() {
modulniOl().catch(() => {});
}
export function BuyruqlarOynasi() {
const oyna = useRef<HTMLDialogElement>(null);
const [ochiq, setOchiq] = useState(false);
const navigate = useNavigate();
function och() {
// flushSync: ichki qism showModal'dan OLDIN DOM'da bo'lsin (chunk
// kelgan bo'lsa — maydon, kelmagan bo'lsa — "Yuklanmoqda…")
flushSync(() => setOchiq(true));
oyna.current?.showModal();
// showModal fokusni birinchi fokuslanadigan elementga
// qo'yadi — cmdk ildizida tabIndex={-1} bor, fokus qidiruv
// maydoniga emas, ildizga tushardi (e2e-20 topdi:
// activeElement — div[cmdk-root]). Shuning uchun — o'zimiz
oyna.current?.querySelector("input")?.focus();
}
function bor(yol: string) {
oyna.current?.close(); // fokus avval ochgan joyga qaytadi...
void navigate(yol); // ...keyin Qobiq yangi sahifa sarlavhasiga qo'yadi
}
// Yorliq hamma joyda, maydon ichida ham ishlaydi: Ctrl+K matn
// yozmaydi. preventDefault — brauzerning o'z Ctrl+K'si (manzil
// qatorida qidiruv). code — jismoniy tugma (W-18): rus tartibida
// key "л", code baribir "KeyK" (key bilan yorliq rus tartibida
// ishlamasdi — e2e-20)
useOynaHodisasi("keydown", (event) => {
if (
(MAC ? event.metaKey : event.ctrlKey) &&
event.code === "KeyK" &&
!event.altKey &&
!event.shiftKey
) {
event.preventDefault();
if (oyna.current?.open === true) {
oyna.current.close();
} else {
och();
}
}
});
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<Tugma
id="buyruqlar-tugma"
aria-haspopup="dialog"
aria-keyshortcuts={MAC ? "Meta+K" : "Control+K"}
onClick={och}
onPointerEnter={oldindanYukla}
>
<Search size={16} />
Buyruqlar
</Tugma>
</TooltipTrigger>
<TooltipContent>{`Tezkor ochish: ${YORLIQ}`}</TooltipContent>
</Tooltip>
<dialog
ref={oyna}
id="buyruqlar"
aria-label="Buyruqlar"
className="w-[min(32rem,100%-2rem)]"
onClose={() => setOchiq(false)}
>
{ochiq && (
<XatoChegarasi
fallback={() => (
<p role="alert">
Buyruqlar oynasini yuklab bo'lmadi — sahifani yangilang.
</p>
)}
>
<Suspense fallback={<p role="status">Yuklanmoqda…</p>}>
<BuyruqlarRoyxati bor={bor} />
</Suspense>
</XatoChegarasi>
)}
</dialog>
</>
);
}Bu darsdagi va oldingi qismlardagi g'oyalar bitta faylda:
- Native
<dialog>— fokus tuzog'i, Esc, fon — brauzerda; cmdk'ningCommand.Dialogi (Radix Dialog) emas, chunki u<style>qo'shadi va CSP'ni buzadi (Modal, drawer…, kanon izohi). lazy+ bitta modul Promise'i —oldindanYuklavalazybir xilimport()ni kutadi, ikki marta yuklanmaydi.pointerenterda oldindan yuklash — sichqoncha tugmaga kelganda chunk tarmoqdan kela boshlaydi, bosilganda tayyor. Tab bilan o'tganlar uchun emas — ular hammasi yuklardi.flushSync— ichki qismshowModal()dan oldin DOM'da bo'lsin (flushSync).- e2e topgan xato — cmdk ildizida
tabIndex={-1}bor:showModal()fokusni maydonga emas, ildizga qo'yardi. Shuning uchuninputga fokus qo'lda; chunk kech kelsa —BuyruqlarRoyxatidagiuseEffect. Suspensefallback —role="status"li «Yuklanmoqda…»;XatoChegarasi— chunk kelmasa (oflayn) aniq xabar (Error boundary).aria-keyshortcutsva tooltip'dagiYORLIQ— «A11y: yorliqni e'lon qilish» bo'limidagi qatlamlar.useOynaHodisasi— 17-qismdagiwindowhodisasi hooki (useEffectEventbilan).event.code === "KeyK"— «keyyokicode?» bo'limidagi qoida. Izoh tuzatish tarixini ham saqlaydi: kim keyinkeyga "soddalashtirmoqchi" bo'lsa, nega bunday qilmaslik kerakligini o'qiydi.
Qobiq.tsx dagi ulanish — oldingi darsda ko'rsatilgan: {kirgan && <BuyruqlarOynasi />} TooltipProvider ichida.
6.4 Narx va tekshiruvlar
Build (tekshiruv-11.txt):
dist/assets/BuyruqlarRoyxati-BZxZmeVJ.js 16.76 kB │ gzip: 6.33 kB
JAMI birinchi js 578660 184776 158829- cmdk qismi — alohida 6.3 KB chunk, faqat oyna ochilganda.
- Birinchi yuklash JS gzip: 181.1 → 184.8 KB (+3.7: Radix tooltip va lazy qobiq). cmdk to'g'ridan import qilinganda +9.0 KB edi.
npm test— node:testpass 198, VitestTests 65 passed (65);lint,format:check,tip,knip—exit 0.- Brauzer (Chrome 155): e2e 159, e2e-18 72, e2e-19 100 — ; e2e-20 — 33 (rus tartibi tekshiruvi bilan). axe — 7 ko'rinishda 0 buzilish, oyna ochiq holda ham (
11-axe.txt).
e2e-20 ning buyruqlar oynasi qismi (natija-20-11.txt, tanlab). Har qatorni darsning bo'limiga bog'lab o'qing:
✅ cmdk: ochilishda yuklanmagan (chunk yo'q, modulepreload ham yo'q): [false,0]
✅ cmdk: tugma fokusi va tooltip — hali yuklanmagan: false
✅ cmdk: chunk kelguncha — oyna ochiq, «Yuklanmoqda…» (role=status): [true,"Yuklanmoqda…"]
✅ cmdk: birinchi Ctrl+K — chunk yuklandi: true
✅ Ctrl+K (maydon ichida ham): oyna modal, fokus qidiruvda (combobox): [true,true,"combobox",true]
✅ bo'sh qidiruv: 4 sahifa + 3 vazifa: 7
✅ qidiruv: «statis» — faqat Statistika: ["Statistika"]
✅ Enter: sahifaga o'tdi, oyna yopildi, fokus sahifa sarlavhasida: [false,"H2","Statistika"]
✅ qidiruv: o‘qish (U+2018) — «Kitob o'qish» topildi: ["Kitob o'qish"]
✅ topilmasa — «Hech narsa topilmadi.»: "Hech narsa topilmadi."
✅ Esc — yopildi, fokus tugmaga qaytdi: [false,"buyruqlar-tugma"]
✅ Ctrl+K rus tartibida (key «л», code KeyK): ochildi, qayta bosish — yopildi: [true,false]
✅ tooltip/Ctrl+K: CSP buzilishi yo'q: []Nima bo'ldi? Kech yuklash, sekin tarmoqda «Yuklanmoqda…» (test chunk'ni +600 ms kechiktirib sinaydi), fokus maydonda, o'zbekcha apostrof, Esc'da fokus qaytishi, rus tartibi, CSP — 0. Rus tartibi qatori tuzatishsiz [false,false] bilan yiqilgan edi (natija-20-11-tuzatishsiz.txt).
6.5 Commit
git add -A
git commitKanonda uch commit (tarix bo'yicha):
feat: Ctrl+K buyruqlar oynasi (cmdk, native dialog), tooltip (Radix)— diff: 6 fayl, +412 −2.perf: buyruqlar oynasi — cmdk birinchi Ctrl+K da yuklanadi (lazy, Suspense)— birinchi yuklashni kamaytirish.fix: Ctrl+K — event.code (rus tartibida ham ishlaydi)— diff: 1 fayl, +4 −2.
Siz yakuniy holatni yozdingiz — bitta commit ham bo'ladi, lekin "nima va nega" uchalasi ham tanasida bo'lsin.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Ctrl+K manzil satrini ochadi | preventDefault() yo'q |
event.preventDefault() |
| rus tartibida yorliq ishlamaydi | event.key bilan tekshirilgan |
event.code === "KeyK" |
| Mac'da yorliq yo'q | faqat ctrlKey |
platformaga qarab metaKey |
«Non olish» yozganda n yorlig'i ishladi |
bitta harfli yorliq maydonda | isTypingTarget / enableOnFormTags |
| «o‘qish» topilmaydi | apostrof varianti | normallovchi filter |
showModal() dan keyin fokus ro'yxat ildizida |
cmdk ildizi tabIndex={-1} |
maydonga qo'lda focus() |
testda ResizeObserver is not defined |
jsdom'da yo'q | sozlash faylida o'rinbosar |
ERESOLVE yoki kutilmagan @radix-ui/react-dialog |
cmdk ^ diapazoni |
overrides |
| birinchi yuklash +9 KB | cmdk to'g'ridan import | lazy() alohida faylda |
8. Mashqlar
Mashqlar kurs/mashqlar/20/11-buyruqlar-oynasi/ da.
1-mashq (oson): to'ldiring
- Mac'da odatiy modifikator xususiyati:
event. - Tartibdan mustaqil tugma tekshiruvi:
event.code === "" - Ekran o'quvchiga yorliqni aytadigan atribut:
- cmdk'da o'z qidiruvingizni beradigan prop:
Yechim
metaKey— Mac'da (Windows'da Win tugmasi).code: "KeyK"— jismoniy joy;keyesa tartibga bog'liq («л»).aria-keyshortcuts="Control+K"(Mac'da"Meta+K").filter={(value, search, keywords) => …}— 1 yoki 0 qaytaradi.
2-mashq (o'rta): sahifa va vazifalar guruhlari
Ikki guruhli buyruqlar oynasini yasang: «Sahifalar» (Ro'yxat, Statistika) va «Vazifalar» (Non olish, Kitob o'qish). Har bandga noyob value (sahifa:/, vazifa:2 …) va qidiruv uchun keywords bering. Filtr — keywords ni normallab, includes bilan. «O‘QISH» (katta harf va ‘) yozilganda faqat «Kitob o'qish» qolsin, Enter bosilganda konsolda → /vazifa/2.
Ishora: «Fuzzy search o'zbekcha matnda» bo'limidagi normalize; kanon mosmi faqat keywords ni tekshiradi.
→ /vazifa/2Yechim
import { Command } from "cmdk";
const PAGES = [
{ path: "/", name: "Ro'yxat" },
{ path: "/statistika", name: "Statistika" },
];
const TASKS = [
{ id: 1, text: "Non olish" },
{ id: 2, text: "Kitob o'qish" },
];
const normalize = (text) =>
text.toLowerCase().replace(/[‘’ʻʼ`]/g, "'");
function matches(_value, search, keywords = []) {
return normalize(keywords.join(" ")).includes(normalize(search))
? 1
: 0;
}
const go = (path) => console.log("→", path);
export default function App() {
return (
<Command label="Buyruqlar" filter={matches} loop>
<Command.Input placeholder="Sahifa yoki vazifa nomi…" />
<Command.List>
<Command.Empty>Hech narsa topilmadi.</Command.Empty>
<Command.Group heading="Sahifalar">
{PAGES.map((p) => (
<Command.Item
key={p.path}
value={`sahifa:${p.path}`}
keywords={[p.name]}
onSelect={() => go(p.path)}
>
{p.name}
</Command.Item>
))}
</Command.Group>
<Command.Group heading="Vazifalar">
{TASKS.map((t) => (
<Command.Item
key={t.id}
value={`vazifa:${t.id}`}
keywords={[t.text]}
onSelect={() => go(`/vazifa/${t.id}`)}
>
{t.text}
</Command.Item>
))}
</Command.Group>
</Command.List>
</Command>
);
}Konsolda:
→ /vazifa/2Filtr value ni (vazifa:2) e'tiborsiz qoldiradi — u faqat kalit. Katta harf va ‘ normallandi. Topilgan yagona band avtomatik tanlangan, shuning uchun Enter darhol uni ochdi. Bo'sh qolgan «Sahifalar» guruhini cmdk o'zi yashiradi.
3-mashq (qiyin): tartibdan mustaqil yorliq hooki
useShortcut(handler) hookini yozing:
windowdagikeydownni tinglaydi (cleanup bilan).- Ctrl+K (Mac'da K) ni
codebo'yicha taniydi,preventDefault()qiladi vahandler()ni chaqiradi. /(modifikatorsiz) hamhandler()ni chaqiradi — lekin maydon ichida emas.
Sinov uchun tugmalar window ga (maydon nomidan ham) hodisa yuboradi: natija oynasida Ctrl bilan tugma bosish qadami yo'q. «Rus» tugmasi key: "л" yuboradi — kanondagi e2e tekshiruvining kichik nusxasi.
Ishora: isPaletteShortcut va shouldHandle — dars bloklaridan; hodisa maydondan yuborilsa, target — maydon (input.dispatchEvent(…) va bubbles: true).
ochildi: Ctrl+K (rus tartibi)
ochildi: /Yechim
import { useEffect, useRef } from "react";
const MAC = /Mac|iPhone|iPad/.test(navigator.platform);
function isTypingTarget(target) {
if (target.isContentEditable) return true;
return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
}
function useShortcut(handler) {
const latest = useRef(handler);
latest.current = handler;
useEffect(() => {
function onKeyDown(event) {
if (event.isComposing) return;
const mod = MAC ? event.metaKey : event.ctrlKey;
if (mod && event.code === "KeyK" && !event.shiftKey) {
event.preventDefault();
latest.current(event);
return;
}
const plain = !event.ctrlKey && !event.metaKey && !event.altKey;
const typing = isTypingTarget(event.target);
if (plain && event.key === "/" && !typing) {
event.preventDefault();
latest.current(event);
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, []);
}
function send(target, init) {
const options = { ...init, bubbles: true };
target.dispatchEvent(new KeyboardEvent("keydown", options));
}
export default function App() {
const input = useRef(null);
useShortcut((event) => {
const name = event.code === "KeyK" ? "Ctrl+K (rus tartibi)" : "/";
console.log("ochildi:", name);
});
return (
<div>
<input ref={input} aria-label="Vazifa nomi" />{" "}
<button
onClick={() =>
send(window, {
key: "л",
code: "KeyK",
ctrlKey: !MAC,
metaKey: MAC,
})
}
>
Ctrl+K (rus)
</button>{" "}
<button onClick={() => send(window, { key: "/" })}>
/ sahifada
</button>{" "}
<button
onClick={() =>
send(input.current, { key: "/" })
}
>
/ maydonda
</button>
</div>
);
}Konsolda:
ochildi: Ctrl+K (rus tartibi)
ochildi: /Uchinchi tugma hech narsa chiqarmadi: hodisa maydondan ko'tarildi, target — INPUT, / modifikatorsiz — e'tiborsiz. Rus tartibida (key: "л") Ctrl+K ishladi — code tufayli. latest ref — effekt bir marta ulanadi, handler esa har doim eng yangisi (latest ref naqshi; kanonda xuddi shu ishni useEffectEvent qiladi). Kanon tuzatishi ham aynan shu bitta qator edi: event.key.toLowerCase() === "k" → event.code === "KeyK". key bilan yozsangiz, «Ctrl+K (rus)» tugmasi hech narsa chiqarmaydi — sinab ko'ring.
4-mashq: Vazifalar qadami
feat/buyruqlar branch'ida: cmdk@1.1.1 va overrides ni qo'shing, BuyruqlarRoyxati.tsx va BuyruqlarOynasi.tsx ni yarating, Qobiq.tsx da ulang (oldingi darsdagi TooltipProvider ichida). Keyin commit.
Yechim
Tekshiruv ro'yxati:
- Build:
BuyruqlarRoyxati-….jsalohida chunk (~6 KB gzip); birinchi yuklash ~+4 KB. Agar ~+9 KB bo'lsa — cmdk to'g'ridan import qilingan. - DevTools Network: sahifa ochilganda cmdk chunk'i yo'q; sichqonchani «Buyruqlar» ustiga olib borganda yoki Ctrl+K da — paydo bo'ladi.
- Ctrl+K (maydon ichida ham) — oyna, fokus qidiruvda; «statis» → Statistika; Enter — sahifa, fokus sarlavhada; Esc — fokus tugmada.
- Network'da "Slow 3G": birinchi ochilishda «Yuklanmoqda…», keyin maydon fokus oladi.
npm test,lint,tip,knip— toza.
9. Real ishda
- Ilg'or foydalanuvchilar — Linear, GitHub, Vercel, Slack: Ctrl+K hamma joyda. Ichki panellarda ham (operator yuzlab buyurtma bilan ishlaydi) — vaqtni ko'p tejaydi.
- Kod ko'rigi — "yorliq maydon ichida ishlab ketmaydimi?", "Mac'da ishlaydimi?", "rus tartibida-chi?" — uch odatiy savol.
- Intervyu savollari: "
keyvacodefarqi?", "combobox naqshi vaaria-activedescendantnima?", "katta komponentni qachon lazy qilasiz?", "fuzzy qidiruvning kamchiligi?".
Xulosa
- cmdk — buyruqlar oynasining ichi:
Command,Input,List,Group,Item,Empty; fokus maydonda, tanlov —aria-activedescendant(combobox naqshi). - Standart fuzzy qidiruv o'zbekcha apostrofni bilmaydi va keraksiz moslik beradi —
filterbilan o'z normallovchi qidiruvingiz;value— noyob kalit,keywords— qidiruv matni. - Ctrl+K: platformaga qarab
ctrlKey/metaKey,code === "KeyK",preventDefault(); bitta harfli yorliqlar maydon vaisComposingda o'chiriladi. - A11y: ochuvchi tugma,
aria-keyshortcuts, ko'rinadigan<kbd>/tooltip; Esc'da fokus qaytadi. vazifalar: native<dialog>+ cmdklazy()alohida chunk'da (6.3 KB),pointerenterda oldindan yuklash; birinchi yuklash +3.7 KB; yorliqcodebilan (rus tartibida ham), e2e-20 33 .
Keyingi dars: Yuklanish, bo'sh va xato holatlari UX — bo'sh ro'yxat ikonka bilan, skeleton 300 ms kechikib va motion-safe animatsiya.
Manbalar
- cmdk — github.com/pacocoursey/cmdk (1.1.1): Command, filter, keywords, vimBindings
- WAI-ARIA APG: Combobox Pattern;
aria-keyshortcuts— w3.org/WAI/ARIA/apg, w3.org/TR/wai-aria - MDN:
KeyboardEvent.code,KeyboardEvent.key,isComposing,navigator.platform— developer.mozilla.org - react-hotkeys-hook — react-hotkeys-hook.vercel.app (5.3.3)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!