Mundarija (33)
- Bu darsda
- 1. Nega bu kerak?
- 2. Telefon: +998 maskasi
- 2.1 Qo'lda yozilgan maska
- 2.2 Kursor muammosi va kutubxonalar
- 3. Raqam: so'm formati
- 3.1 Intl.NumberFormat — va tuzoq
- 3.2 react-number-format
- 4. OTP: bir martalik kod
- 4.1 Bitta maydon
- 4.2 Katakchalar baribir kerak bo'lsa
- 5. Sana
- 5.1 Avval brauzerning o'zi
- 5.2 Vaqt mintaqasi tuzog'i
- 5.3 Qachon kutubxona
- 6. Combobox: takliflar ro'yxatli maydon
- 6.1 Naqsh
- 6.2 Kichik combobox
- 6.3 Kutubxonalar
- 7. Nega vazifalar da telefon, sana va OTP yo'q
- 8. Vazifalar qadami: teg combobox'i (holat 35)
- 8.1 ui/Combobox.tsx
- 8.2 Wizard'ga ulash
- 8.3 Tekshiruvlar va commit
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi atribut?
- 2-mashq (o'rta): karta raqami
- 3-mashq (qiyin): taom combobox'i — RHF bilan
- 4-mashq: Vazifalar qadami
- 11. Real ishda
- Xulosa
- Manbalar
React'da maxsus inputlar: telefon maskasi, so'm formati, OTP, sana va combobox
Qisqacha: Maxsus input — ko'rinishi va qiymati farq qiladigan maydon: ekranda
+998 (90) 123-45-67, formada998901234567; ekranda35 000, formada35000. Asosiy qoida: formada xom qiymat, ekranda formatlangan, ulanish —Controllerorqali. Telefon va raqam uchuninputModevaautoComplete, OTP uchun bitta maydon vaautocomplete="one-time-code", sana uchun avval brauzerning<input type="date">i. Combobox (takliflar ro'yxatli maydon) — ARIA 1.2 naqshi: fokus maydonda qoladi, faol taklif —aria-activedescendant.
Bu darsda
- +998 telefon maskasi va so'm formatidagi raqam maydonini qo'lda yozasiz, xom qiymatni formaga berasiz.
- OTP (bir martalik kod) maydonini telefon klaviaturasi va SMS avto-to'ldirishga mos qilasiz.
- Brauzerning sana maydonini ishlatasiz va vaqt mintaqasi tuzog'ini bilasiz.
- ARIA combobox naqshini tushunasiz va
vazifalardagi teg combobox'ini o'qiysiz. - Qaysi vaziyatda tayyor kutubxona (imask, input-otp, react-number-format, react-day-picker, Downshift) kerakligini bilasiz.
Oldin bilishingiz kerak: RHF II: Controller, RHF III, Intl.NumberFormat, Fokus boshqaruvi va klaviatura navigatsiyasi.
1. Nega bu kerak?
«Bahor» bron formasida Jasur aka to'rtta shikoyat yig'di:
- Mehmonlar telefonni har xil yozadi:
901234567,+998 90 123 45 67,90-123-45-67. Operator qo'ng'iroq qila olmaydi. - Narx maydonida
350000ni o'qish qiyin —35 000mi,350 000mi? - SMS kod maydoniga telefon harflar klaviaturasini ochadi, kod SMS'dan o'zi tushmaydi.
- Teg maydonida mehmon
#bozoro'rniga#bazoryozadi — takliflar ro'yxati kerak.
Bularning hammasi — maxsus input: oddiy <input> ustiga xulq qo'shilgan maydon. Hayotiy o'xshatish: bank kartasi formasidagi raqam joyi. Siz 16 ta raqam yozasiz, u esa ularni to'rttalab ajratib ko'rsatadi — 8600 1234 5678 9012. Bank esa bo'shliqsiz 16 raqamni oladi. Ko'rinish — odam uchun, qiymat — mashina uchun.
Asosiy qoida butun dars davomida bitta:
flowchart LR
K["Mehmon yozdi<br/>90 123 45 67"] --> X["Xom qiymat<br/>(formada)<br/>998901234567"]
X --> F["Formatlangan<br/>(ekranda)<br/>+998 (90) 123-45-67"]
X --> S["Server, sxema,<br/>saqlash"]Format — faqat ekranda. Sxema, server va localStorage xom qiymatni ko'radi.
2. Telefon: +998 maskasi
2.1 Qo'lda yozilgan maska
Maska (mask) — maydon qiymatini qolipga solish: +998 (__) ___-__-__. Mehmon faqat raqam yozadi, qavs va chiziqchalar o'zi qo'yiladi. Ikki sof funksiya kerak: "ekrandagi matndan raqamlarni ol" va "raqamlardan ko'rinish yasa":
import { Controller, useForm } from "react-hook-form";
// "+998 (90) 123-45-67" → "901234567" (998 siz, ko'pi bilan 9 ta)
function toDigits(text) {
const digits = text.replace(/\D/g, "");
const local = digits.startsWith("998") ? digits.slice(3) : digits;
return local.slice(0, 9);
}
// "901234567" → "+998 (90) 123-45-67"
function formatPhone(d) {
if (d === "") return "";
let out = "+998 (" + d.slice(0, 2);
if (d.length > 2) out += ") " + d.slice(2, 5);
if (d.length > 5) out += "-" + d.slice(5, 7);
if (d.length > 7) out += "-" + d.slice(7, 9);
return out;
}
export default function App() {
const { control, handleSubmit } = useForm({
defaultValues: { phone: "" },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<label htmlFor="phone">Telefon</label>
<Controller
control={control}
name="phone"
render={({ field }) => (
<input
id="phone"
ref={field.ref}
type="tel"
inputMode="tel"
autoComplete="tel-national"
placeholder="+998 (90) 000-00-00"
value={formatPhone(field.value)}
onChange={(e) => field.onChange(toDigits(e.target.value))}
onBlur={field.onBlur}
/>
)}
/>
<button>Bron qilish</button>
</form>
);
}Konsolda:
{phone: '901234567'}Mehmon 90 123 45 67 yozdi — maydonda +998 (90) 123-45-67 (input qiymati "Ekranda" ga kirmaydi, shuning uchun bu yerda ko'rsatilmagan; «Ishga tushir» da ko'rasiz), formada esa 901234567. Uch muhim tafsilot:
toDigits998bilan boshlangan matnni ham to'g'ri o'qiydi — mehmon+998 90 …ni to'liq yozsa yoki nusxalab qo'ysa (paste).type="tel"vainputMode="tel"— telefonda raqamli klaviatura ochiladi.type="number"emas: telefon raqami "son" emas (boshida0bo'lishi,+belgisi), vanumbermaydoni strelkalar bilan qiymatni o'zgartiradi.autoComplete="tel-national"— brauzer saqlangan raqamni taklif qiladi (mamlakat kodisiz qism).
Sxemada xom qiymat tekshiriladi: z.string().regex(/^\d{9}$/, "To'liq raqam yozing") (Zod chuqurroq). Serverga — "998" + phone.
2.2 Kursor muammosi va kutubxonalar
Qo'lda yozilgan maskaning bitta zaif joyi bor — kursor. Mehmon raqamning o'rtasini tuzatsa (123 dagi 2 ni o'chirsa), React qiymatni qayta formatlaydi va kursor oxiriga sakraydi. Buni to'g'rilash — selectionStart ni hisoblab qayta qo'yish, chiziqchalarni hisobga olish. Mayda, lekin chalkash ish.
Shuning uchun real loyihada maska — kutubxona:
| Kutubxona | Nima uchun |
|---|---|
react-imask (IMask) |
har qanday maska: telefon, karta, sana; kursorni to'g'ri boshqaradi |
maska |
kichik, faqat maska (Vue va vanilla ham) |
react-number-format |
raqam va valyuta + PatternFormat (telefon) |
react-imask bilan ko'rinishi (bu kutubxona natija oynasida yo'q — o'z loyihangizda):
import { IMaskInput } from "react-imask";
<Controller
control={control}
name="phone"
render={({ field }) => (
<IMaskInput
mask="+{998} (00) 000-00-00"
unmask={true}
inputRef={field.ref}
value={field.value}
onAccept={(value) => field.onChange(value)}
onBlur={field.onBlur}
/>
)}
/>unmask={true} — onAccept xom raqamlarni beradi (998901234567), 0 — "bu yerga raqam", {998} — o'zgarmas qism. Yana Controller (RHF II): kutubxona komponentida value/onChange o'z nomlari bilan.
Tekshirib ko'ring: Nega
field.valuega formatlangan matnni (+998 (90) 123-45-67) saqlamadik?
Javob
Uch sabab. Sxema va server bitta ko'rinishni tekshiradi — \d{9}, qavs va chiziqchasiz. Ko'rinish keyin o'zgarishi mumkin (90 123 45 67 ko'rinishiga o'tish), saqlangan ma'lumot esa o'zgarmasin. Va isDirty: formatlangan matn har xil yozilishi mumkin, xom raqam esa bitta. Ko'rinish — value={formatPhone(...)} da, render paytida hisoblanadi.
3. Raqam: so'm formati
3.1 Intl.NumberFormat — va tuzoq
So'm uchun minglik ajratuvchi kerak: 35 000. Brauzerda buning vositasi — Intl.NumberFormat. Lekin uning natijasi brauzer va Node'da, hatto brauzer versiyalari orasida farq qilishi mumkin: ajratuvchi oddiy bo'shliq, "uzilmas bo'shliq" (U+00A0), "ingichka uzilmas bo'shliq" (U+202F) yoki vergul bo'ladi. Bizning tekshiruvchimiz (Node) va saytdagi Chrome ham bir xil chiqarmasligi mumkin — shuning uchun bu darsda ekranga chiqadigan formatni o'zimiz yozamiz:
import { Controller, useForm } from "react-hook-form";
const groupDigits = (s) => s.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
const showPrice = (n) => (n === null ? "" : groupDigits(String(n)));
export default function App() {
const { control, handleSubmit } = useForm({
defaultValues: { price: null },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<label htmlFor="price">Narxi</label>
<Controller
control={control}
name="price"
render={({ field }) => (
<span>
<input
id="price"
ref={field.ref}
inputMode="numeric"
autoComplete="off"
value={showPrice(field.value)}
onChange={(e) => {
const digits = e.target.value.replace(/\D/g, "");
field.onChange(digits === "" ? null : Number(digits));
}}
onBlur={field.onBlur}
/>{" "}
so'm
</span>
)}
/>
<button>Saqlash</button>
</form>
);
}Konsolda:
{price: 35000}groupDigits — muntazam ifoda (showPrice — null uchun bo'sh satr): "o'ng tomonida uch xonali guruhlar qolgan har chegaraga bo'shliq qo'y" (RegExp). Formada son (35000), bo'sh maydon — null (Zod chuqurroq dagi "" → 0 tuzog'iga tushmaslik uchun). inputMode="numeric" — telefonda faqat raqamlar klaviaturasi; type="number" emas, chunki unda bo'shliqli matn ko'rinmaydi.
Saytning boshqa joyida (ro'yxat, chek) — Intl.NumberFormat("uz-UZ") ishlatish to'g'ri: u faqat ko'rsatadi, tekshiruvchi yo'q. Lekin solishtirish yoki testda uning natijasiga tayanmang.
3.2 react-number-format
Kasr qism (12 500,50), manfiy son, valyuta belgisi, kursor — react-number-format ning NumericFormat komponenti:
<NumericFormat
thousandSeparator=" "
suffix=" so'm"
allowNegative={false}
decimalScale={0}
value={field.value}
onValueChange={(v) => field.onChange(v.floatValue ?? null)}
/>v.floatValue — xom son, v.formattedValue — ekrandagi matn. Xuddi o'sha qoida: formaga — xom qiymat.
4. OTP: bir martalik kod
4.1 Bitta maydon
OTP (one-time password) — SMS yoki ilovaga keladigan bir martalik kod (masalan, 6 raqam). Ko'p saytlar uni 6 ta alohida katakcha qilib chizadi. Chiroyli, lekin muammoli: nusxalab qo'yish (paste) ishlamaydi, ekran o'quvchi "6 ta tahrirlash maydoni" ni o'qiydi, SMS'dan avto-to'ldirish bitta katakka tushadi.
Ishonchli yo'l — bitta maydon, to'g'ri atributlar bilan:
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, setValue } = useForm({
defaultValues: { code: "" },
});
const verify = handleSubmit((d) => console.log("kod:", d.code));
const field = register("code", {
pattern: /^\d{6}$/,
onChange: (e) => {
const digits = e.target.value.replace(/\D/g, "").slice(0, 6);
setValue("code", digits);
if (digits.length === 6) void verify();
},
});
return (
<form onSubmit={verify}>
<label htmlFor="code">SMS kod</label>
<input
id="code"
inputMode="numeric"
autoComplete="one-time-code"
maxLength={6}
{...field}
/>
<button>Tasdiqlash</button>
</form>
);
}Konsolda:
kod: 123456Atributlar ishni qiladi:
autoComplete="one-time-code"— iPhone'da SMS'dagi kod klaviatura ustida taklif bo'lib chiqadi, Android'da ham shunga o'xshash. Bir bosish — kod maydonda.inputMode="numeric"— raqamli klaviatura.maxLength={6}— ortiqcha belgi yozilmaydi.
register ning onChange opsiyasi — RHF ning o'z ishlovidan keyin chaqiriladi: RHF qiymatni ("12 34 56") allaqachon olgan. Shuning uchun tozalangan raqamlarni setValue bilan qaytaramiz — u forma qiymatini ham, maydonning o'zini ham yangilaydi (RHF II). 6 raqam to'lganda forma o'zi yuboriladi — "Tasdiqlash" ni bosish shart emas. Biz avval e.target.value = digits deb yozib ko'rdik: maydonda raqamlar to'g'ri ko'rindi, lekin RHF eski qiymatni tekshirdi va pattern xatosi bilan yubormadi.
Brauzerda yana bir imkoniyat bor — WebOTP API (navigator.credentials.get({ otp: … })): Android Chrome SMS'ni ruxsat bilan o'zi o'qiydi. SMS maxsus formatda bo'lishi kerak (@sizning-sayt.uz #123456 qatori bilan) — bu backend bilan kelishiladi.
4.2 Katakchalar baribir kerak bo'lsa
Dizayner 6 ta katakchani talab qilsa, input-otp kutubxonasi yaxshi yo'l: u ichida bitta ko'rinmas <input> ishlatadi, katakchalar esa faqat chizma. Paste, avto-to'ldirish va ekran o'quvchi ishlaydi:
import { OTPInput } from "input-otp";
<OTPInput
maxLength={6}
value={field.value}
onChange={field.onChange}
onComplete={() => void verify()}
render={({ slots }) =>
slots.map((slot, i) => <Slot key={i} {...slot} />)
}
/>shadcn/ui ning InputOTP komponenti aynan shu kutubxona ustiga qurilgan (shadcn/ui I).
Diqqat: OTP kodini brauzerda tekshirib bo'lmaydi — kodni faqat server biladi. Brauzer qiladigan ish: format (6 raqam) va yuborish. Urinishlar sonini cheklash ("5 marta noto'g'ri — 10 daqiqa kuting") ham — server ishi (CAPTCHA va bot himoyasi).
5. Sana
5.1 Avval brauzerning o'zi
<input type="date"> — brauzerning o'z kalendari: telefonda tizim tanlagichi, kompyuterda ochiladigan oyna, klaviatura bilan ishlaydi, ekran o'quvchiga tanish. Qiymati — doim "YYYY-MM-DD" satri, ko'rinishi esa brauzer tiliga qarab (14.10.2026 yoki 10/14/2026):
import { useForm } from "react-hook-form";
const today = "2026-10-14";
export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ defaultValues: { date: "" } });
return (
<form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
<label htmlFor="date">Bron sanasi</label>
<input
id="date"
type="date"
min={today}
{...register("date", {
required: "Sanani tanlang",
validate: (v) =>
v >= today || "O'tgan kunni tanlab bo'lmaydi",
})}
/>
<p role="alert">{errors.date?.message}</p>
<button>Bron qilish</button>
</form>
);
}Konsolda:
{date: '2026-10-20'}"YYYY-MM-DD" satrlarni oddiy >= bilan solishtirish mumkin — tartibi kalendar tartibiga mos (Taqqoslash operatorlari). Kalendardagi min o'tgan kunlarni tanlatmaydi. Lekin bu faqat maslahat: qo'lda yozilgan yoki skript bilan qo'yilgan qiymat uchun validate kerak (server ham baribir tekshiradi). Bu yerda today o'zgarmas, toki misol har kuni bir xil natija bersin. Loyihada uni joriy sanadan hisoblaysiz.
5.2 Vaqt mintaqasi tuzog'i
"2026-10-20" ni Date ga aylantirmoqchi bo'lsangiz, ehtiyot bo'ling:
const date = new Date("2026-10-20");
console.log(date.toISOString()); // 2026-10-20T00:00:00.000Z
console.log(new Date("2026-10-20T00:00").getDate()); // 20new Date("2026-10-20") — UTC yarim tuni. Toshkentda (UTC+5) u 05:00 bo'ladi, AQShda esa 19-oktabr kechqurun. Sana "kun" bo'lsa (bron, tug'ilgan kun), uni satr holida saqlang va serverga satr qilib yuboring. Date ga aylantirish kerak bo'lsa — vaqt qismi bilan, mahalliy vaqtda ("…T00:00"). Sanalar bilan ishlash chuqur — Temporal darsida.
5.3 Qachon kutubxona
Brauzer kalendari yetmaydigan joylar: band kunlarni kulrang qilish, sanalar oralig'i (kelish — ketish), bir nechta oyni yonma-yon ko'rsatish, o'z dizayni. Unda — react-day-picker (shadcn/ui ning Calendar i shu):
import { DayPicker } from "react-day-picker";
<DayPicker
mode="range"
selected={field.value}
onSelect={field.onChange}
disabled={[{ before: new Date() }, ...bookedDays]}
/>Kalendar va bron UI'i alohida darsda — Kalendar va bron qilish UI.
6. Combobox: takliflar ro'yxatli maydon
6.1 Naqsh
Combobox — matn maydoni + ochiladigan takliflar ro'yxati. Mehmon yozadi, mos takliflar chiqadi, birini tanlaydi yoki o'zinikini yozib qoldiradi. Bu <select> emas (o'z qiymatini yozish mumkin) va <datalist> emas (uning ko'rinishi va klaviaturasi brauzerga bog'liq, ekran o'quvchilarda notekis).
Uni to'g'ri qurish uchun WAI-ARIA 1.2 combobox naqshi (APG — ARIA Authoring Practices Guide) bor. Asosiy qoidalar:
| Atribut / xulq | Nima uchun |
|---|---|
maydonda role="combobox" |
ekran o'quvchi "takliflar bor maydon" deb biladi |
aria-expanded |
ro'yxat ochiqmi |
aria-controls |
qaysi ro'yxatni boshqaradi (id) |
aria-autocomplete="list" |
takliflar yozilganiga qarab filtrlanadi |
ro'yxat role="listbox", bandlar role="option" |
tanlanadigan variantlar |
| fokus doim maydonda | mehmon yozishda davom eta oladi |
aria-activedescendant |
↓/↑ bilan "faol" bo'lgan variant id'si — ekran o'quvchi uni o'qiydi |
| ↓ ↑ Enter Esc | ↓/↑ — variant, Enter — tanlash (forma yuborilmaydi), Esc — yopish |
Eng nozik nuqta — fokus. Takliflar ro'yxatiga fokus o'tmaydi (u Tab bilan o'tadigan narsa emas). Fokus maydonda qoladi, "qaysi variant faol" esa aria-activedescendant bilan aytiladi. Bobur ↓ bosganda ham yozishda davom eta oladi.
6.2 Kichik combobox
«Bahor» teglari uchun. Klaviatura qismi ham yozilgan, lekin bizning tekshiruvchi tugma bosa olmaydi — ↓ va Enter ni «Ishga tushir» da o'zingiz sinang. Tekshiruvchi yozadi va sichqoncha bilan tanlaydi:
import { useId, useState } from "react";
const TAGS = ["#bozor", "#bolalar", "#uy", "#ta'mir"];
function TagCombobox({ value, onChange }) {
const listId = useId();
const [open, setOpen] = useState(false);
const [active, setActive] = useState(-1);
const query = value.toLowerCase();
const matches = TAGS.filter(
(t) => t.startsWith(query) && t !== value,
);
const shown = open && matches.length > 0;
function choose(tag) {
onChange(tag);
setOpen(false);
setActive(-1);
}
function onKeyDown(e) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (matches.length === 0) return;
const step = e.key === "ArrowDown" ? 1 : -1;
setOpen(true);
setActive((a) => (a + step + matches.length) % matches.length);
} else if (e.key === "Enter" && shown && active >= 0) {
e.preventDefault(); // forma yuborilmasin — faqat tanlash
choose(matches[active]);
} else if (e.key === "Escape" && shown) {
e.preventDefault();
setOpen(false);
}
}
return (
<span>
<label htmlFor="tag">Teg</label>
<input
id="tag"
role="combobox"
aria-autocomplete="list"
aria-expanded={shown}
aria-controls={listId}
aria-activedescendant={
shown && active >= 0 ? `${listId}-${active}` : undefined
}
autoComplete="off"
value={value}
onChange={(e) => {
onChange(e.target.value);
setOpen(true);
setActive(-1);
}}
onKeyDown={onKeyDown}
onBlur={() => setOpen(false)}
/>
<ul id={listId} role="listbox" hidden={!shown}>
{matches.map((tag, i) => (
<li
key={tag}
id={`${listId}-${i}`}
role="option"
aria-selected={i === active}
onMouseDown={(e) => e.preventDefault()}
onClick={() => choose(tag)}
>
{tag}
</li>
))}
</ul>
</span>
);
}
export default function App() {
const [tag, setTag] = useState("");
return (
<form>
<TagCombobox value={tag} onChange={setTag} />
<p>Tanlandi: {tag}</p>
</form>
);
}Ekranda:
Teg
Tanlandi: #bolalar#b yozildi — ikki taklif (#bozor, #bolalar) ro'yxatda (tekshiruvchi #bozor ni ko'rdi). #bolalar bosildi — tanlandi, ro'yxat yopildi va endi hidden (ekranga kirmaydi). Uch nozik joy:
onMouseDown={(e) => e.preventDefault()}— sichqoncha bosilganda fokus maydondan ketmasin. Aks holda avvalblurishlaydi, ro'yxat yopiladi vaclick"yo'q" elementga tushadi. Bu — combobox'larning eng ko'p uchraydigan xatosi.hidden={!shown}— ro'yxat DOM'da turadi (aria-controlsuni topsin), lekin ko'rinmaydi.- Enter faqat tanlaganda
preventDefault— faol variant yo'q bo'lsa, Enter odatdagidek formani yuboradi.
6.3 Kutubxonalar
Combobox — a11y jihatdan eng qiyin vidjetlardan biri: brauzer va ekran o'quvchi juftliklari har xil. Real loyihada ko'pincha tayyor yechim:
| Kutubxona | Nima |
|---|---|
| Downshift | headless combobox/select hook'lari (useCombobox) — ARIA'ni o'zi qo'yadi, ko'rinish sizniki |
| cmdk | "buyruqlar palitrasi" (K) — qidiruvli ro'yxat; shadcn/ui Command |
| React Aria, Radix, Headless UI | to'liq a11y komponentlar (Radix Primitives) |
Downshift ning ko'rinishi — headless komponentlar naqshi:
const { isOpen, getInputProps, getMenuProps, getItemProps } =
useCombobox({ items: matches, onInputValueChange: … });
<input {...getInputProps()} />
<ul {...getMenuProps()}>
{isOpen && matches.map((item, index) => (
<li key={item} {...getItemProps({ item, index })}>{item}</li>
))}
</ul>getInputProps() — role, aria-*, klaviatura ishlovchilari hammasi bir yerda (prop getters). Bizning 60 qatorlik kodimiz — Downshift ichida shu qadar ish.
Tekshirib ko'ring: Nega takliflar ro'yxatiga fokus o'tkazmaymiz (masalan, ↓ bosilganda birinchi
<li>gafocus())?
Javob
Fokus ro'yxatga o'tsa, mehmon yozishni davom ettira olmaydi — harflar endi maydonga tushmaydi. Ekran o'quvchida ham kontekst yo'qoladi ("maydon" dan "ro'yxat" ga sakrash). ARIA combobox naqshi shuning uchun fokusni maydonda qoldiradi va faol variantni aria-activedescendant bilan bildiradi. vazifalar dagi eski teg takliflari (14-qismda) aynan fokusni tugmalarga o'tkazar edi — TEXNIK-QARZ'dagi 20-band.
7. Nega vazifalar da telefon, sana va OTP yo'q
Bu darsdagi beshta inputdan vazifalar ga faqat combobox kirdi. Telefon, sana va OTP — yo'q, va bu ataylab.
Sabab — ma'lumot modeli. Vazifada uch maydon bor: id, matn, bajarildi. Telefon yoki muddat (sana) qo'shish — modelni o'zgartirish. U ko'p joyga tegadi: localStorage dagi saqlash formati (PAKET_VERSIYASI), keshdagi nusxa, «Bahor» mashq serverining API'si, import/eksport fayli, 13–19-qismlarning brauzer tekshiruvlari. Heap (navbat) qadami ham 14-qismda aynan shu sabab bilan qilinmagan edi: "muddat maydoni ma'lumot modelini o'zgartiradi". Kursning loyihasi o'quv uchun — har dars uchun yangi maydon qo'shsak, model "hamma narsani saqlaydigan" bo'lib qoladi.
Teglar esa allaqachon bor (12-qismdan): ular matn ichida (#bozor). Combobox modelni o'zgartirmaydi — u faqat mavjud teglarni taklif qiladi. Telefon, sana va OTP o'z o'rnida chiqadi: OTP — haqiqiy auth bilan (28-qism), sana tanlagich — Kalendar va bron darsida.
8. Vazifalar qadami: teg combobox'i (holat 35)
8.1 ui/Combobox.tsx
Kanon, to'liq (izoh qatorlari telefonga sig'ishi uchun bo'lingan):
// Combobox.tsx — maxsus input (19/#35): matn maydoni + takliflar
// ro'yxati, ARIA 1.2 combobox (APG: "list autocomplete, manual
// selection"). Fokus DOIM maydonda qoladi; ro'yxatdagi "faol" taklif
// — aria-activedescendant (ekran o'quvchi uni o'qiydi). Klaviatura:
// ↓/↑ — taklif, Enter — tanlash (forma yuborilmaydi), Esc — yopish,
// Tab — yopib keyingi elementga.
// 14/#41 dagi teg takliflari (tugmalar guruhi) — to'liq combobox emas
// edi (TEXNIK-QARZ 20): yangi formalar shu komponentni ishlatadi
import { useId, useState } from "react";
import type { KeyboardEvent, Ref } from "react";
type Props = {
id: string;
qiymat: string;
onOzgar: (qiymat: string) => void;
onBlur?: () => void;
ref?: Ref<HTMLInputElement>;
/** hamma variant — tartibi bilan (filtrlash shu yerda) */
takliflar: readonly string[];
placeholder?: string;
"aria-invalid"?: boolean | undefined;
"aria-describedby"?: string | undefined;
};
const ENG_KOP = 8;
export function Combobox({
id,
qiymat,
onOzgar,
onBlur,
ref,
takliflar,
placeholder,
...aria
}: Props) {
const royxatId = useId();
const [ochiq, setOchiq] = useState(false);
const [faol, setFaol] = useState(-1); // -1 — hech biri
const sorov = qiymat.trim().toLowerCase();
const mos = takliflar
.filter((t) => t.toLowerCase().startsWith(sorov) && t !== qiymat)
.slice(0, ENG_KOP);
const korinadi = ochiq && mos.length > 0;
const variantId = (i: number) => `${royxatId}-${i}`;
function tanla(taklif: string) {
onOzgar(taklif);
setOchiq(false);
setFaol(-1);
}
function tugma(event: KeyboardEvent<HTMLInputElement>) {
if (event.nativeEvent.isComposing) {
return;
}
switch (event.key) {
case "ArrowDown":
case "ArrowUp": {
event.preventDefault();
if (mos.length === 0) {
return;
}
setOchiq(true);
const qadam = event.key === "ArrowDown" ? 1 : -1;
setFaol((f) => (f + qadam + mos.length) % mos.length);
return;
}
case "Enter": {
const tanlangan = korinadi ? mos[faol] : undefined;
if (tanlangan !== undefined) {
// formani yubormasin — faqat tanlash
event.preventDefault();
tanla(tanlangan);
}
return;
}
case "Escape":
if (korinadi) {
event.preventDefault();
setOchiq(false);
setFaol(-1);
}
return;
default:
}
}
return (
<span className="combobox">
<input
ref={ref}
id={id}
type="text"
role="combobox"
autoComplete="off"
placeholder={placeholder}
aria-autocomplete="list"
aria-expanded={korinadi}
aria-controls={royxatId}
aria-activedescendant={
korinadi && faol >= 0 ? variantId(faol) : undefined
}
value={qiymat}
onChange={(event) => {
onOzgar(event.target.value);
setOchiq(true);
setFaol(-1);
}}
onKeyDown={tugma}
onBlur={() => {
setOchiq(false);
setFaol(-1);
onBlur?.();
}}
{...aria}
/>
<ul id={royxatId} role="listbox" hidden={!korinadi}>
{mos.map((taklif, i) => (
<li
key={taklif}
id={variantId(i)}
role="option"
aria-selected={i === faol}
// mousedown'da fokus maydondan ketmasin (blur ro'yxatni
// yopardi)
onMouseDown={(event) => event.preventDefault()}
onClick={() => tanla(taklif)}
>
{taklif}
</li>
))}
</ul>
</span>
);
}«Kichik combobox» bo'limidagi kodning to'liq varianti. Qo'shimchalar:
event.nativeEvent.isComposing— kirill yoki xitoy kabi kiritish usullarida (IME) harf "yig'ilayotgan" paytda Enter va strelkalar shu usulga tegishli. Ularni ushlamaymiz.ENG_KOP = 8— 200 ta teg bo'lsa ham ro'yxat uzun bo'lmasin....aria— forma berganaria-invalidvaaria-describedbyto'g'ridan maydonga (komponent API'si).ref— React 19 prop:Controllerningfield.refi maydonga yetadi, xatoda fokus shu yerga.
8.2 Wizard'ga ulash
YangiLoyiha.tsx dagi teg maydoni register dan Controller ga o'tdi (kanon diff):
+ // 19/#35: mavjud teglar — ko'p ishlatilgani oldin (combobox
+ // takliflari)
+ const teglar = useMemo(
+ () =>
+ [...VazifalarRoyxati.dan(vazifalar).tegHisobi()]
+ .toSorted((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
+ .map(([teg]) => teg),
+ [vazifalar],
+ );
...
- <input
- id="usta-teg"
- autoComplete="off"
- placeholder="#ta'mir"
- aria-invalid={
- tegXatosi === undefined ? undefined : true
- }
- aria-describedby={tegXatoId}
- {...register("teg")}
+ {/* 19/#35: maxsus input — combobox (Controller orqali:
+ qiymat va o'zgarish props bilan) */}
+ <Controller
+ control={control}
+ name="teg"
+ render={({ field }) => (
+ <Combobox
+ id="usta-teg"
+ ref={field.ref}
+ qiymat={field.value}
+ onOzgar={field.onChange}
+ onBlur={field.onBlur}
+ takliflar={teglar}
+ placeholder="#ta'mir"
+ aria-invalid={
+ tegXatosi === undefined ? undefined : true
+ }
+ aria-describedby={tegXatoId}
+ />
+ )}
/>- Takliflar tartibi — eng ko'p ishlatilgan teg birinchi (
tegHisobi()— teg → nechta vazifada), teng bo'lsa alifbo bo'yicha.toSorted— massivni o'zgartirmaydigan saralash (toSorted). Controller— RHF II dagiAlmashtirgichbilan bir xil ulanish:qiymat/onOzgar— komponentning o'z nomlari.- Sxema o'zgarmadi — combobox faqat yozishni osonlashtiradi, qoida (
#bilan, bo'shliqsiz) o'sha.
CSS (vazifalar.css, 20 qator) — ro'yxat maydon ostida, position: absolute, faol variant — asosiy rang fonida.
8.3 Tekshiruvlar va commit
Yangi Vitest fayli tekshiruv/Combobox.test.tsx — 3 test: takliflar va aria-expanded; ↓ ↓ ↑ — aria-activedescendant, Enter tanlaydi va formani yubormaydi; Esc va sichqoncha bilan tanlash. Ikkinchisining yakuni (kanon):
fireEvent.keyDown(maydon(), { key: "Enter" });
expect(maydon().value).toBe("#bozor");
expect(maydon().getAttribute("aria-expanded")).toBe("false");
expect(yuborildi).not.toHaveBeenCalled();yuborildi — formaning onSubmit "josusi" (vi.fn()): Enter tanladi, lekin forma yuborilmadi.
Natijalar (haqiqiy, holat 35):
npm test— node:test 187, VitestTests 67 passed (67)(+3). «Orqaga» tuzatishidan keyin sizda node:test — 195.- Build:
index-….js 475.31 kB │ gzip: 151.40 kB— +0.6 KB (kutubxonasiz, o'z komponentimiz). - Brauzer: 159/159, 72/72, 19-qism — 89 (tuzatishlardan keyin 92). Combobox bo'limi (haqiqiy Chrome'da, klaviatura bilan):
✅ combobox: yozildi — takliflar (listbox), faol yo'q: {"rol":"combobox","ochiq":"true","takliflar":["#bozor"],"faol":null,"qiymat":"#b","fokus":true}
✅ combobox: ↓ — faol taklif (aria-activedescendant), fokus maydonda: {"rol":"combobox","ochiq":"true","takliflar":["#bozor"],"faol":"#bozor","qiymat":"#b","fokus":true}
✅ combobox: Enter — tanlandi, yopildi, qadam o'zgarmadi (forma yuborilmadi): [{"rol":"combobox","ochiq":"false","takliflar":[],"faol":null,"qiymat":"#bozor","fokus":true},"Teg"]
✅ combobox: CSP buzilishi yo'q: []Har qatorda "fokus":true — fokus butun jarayonda maydonda qoldi. Diff: 4 fayl, +267 −11.
git switch -c feat/teg-combobox
git add -A
git commitCommit (kanon): feat: teg combobox (ARIA 1.2) — ko'p qadamli formada. Ro'yxat sahifasidagi eski teg takliflari hali o'zgarmagan (TEXNIK-QARZ 20 — "yangi formalar shu komponentni ishlatadi").
9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
formada "+998 (90) …" saqlanyapti |
formatlangan matn qiymat qilib berilgan | formaga xom raqam, format — value da |
| telefonda harfli klaviatura | inputMode yo'q |
inputMode="tel" / "numeric" |
telefon maydonida strelkalar, 0 yo'qoldi |
type="number" |
type="tel" |
| SMS kod o'zi tushmaydi | autoComplete yo'q |
autoComplete="one-time-code" |
| 6 katakchaga paste ishlamaydi | har katak — alohida input | bitta input yoki input-otp |
| sana bir kun oldin chiqdi | new Date("YYYY-MM-DD") — UTC |
sanani satr holida saqlang |
| taklif bosilganda tanlanmaydi | blur ro'yxatni yopdi |
onMouseDown={(e) => e.preventDefault()} |
| Enter taklifni tanlash o'rniga formani yubordi | preventDefault yo'q |
Enter — faol variant bo'lsa preventDefault |
| ekran o'quvchi faol variantni aytmaydi | aria-activedescendant yo'q yoki id noto'g'ri |
variant id'si — useId asosida |
10. Mashqlar
Mashqlar kurs/mashqlar/19/35-maxsus-inputlar/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi atribut?
- Telefonda raqamli klaviatura (telefon raqami uchun) →
inputMode="" - SMS'dagi kodni taklif qilish →
autoComplete="" <input type="date">qiymatining ko'rinishi →- Combobox'da faol variant id'si turadigan atribut →
- Combobox ro'yxati ochiqligini bildiradigan atribut →
Yechim
tel— raqamlar va+,*,#. Faqat raqam kerak bo'lsa —numeric.one-time-code— iOS va Android SMS'dan kodni taklif qiladi.YYYY-MM-DD— ko'rinish brauzer tiliga qarab, qiymat doim shu.aria-activedescendant— fokus maydonda, "faol" — shu id.aria-expanded—true/false.
2-mashq (o'rta): karta raqami
To'lov formasiga karta raqami maydoni: ekranda to'rttalab (8600 1234 5678 9012), formada 16 raqam ("8600123456789012"). 16 raqamdan kam bo'lsa — "Karta raqami 16 ta raqam" (sxema yoki validate). «Telefon: +998 maskasi» bo'limidagi ikki funksiya g'oyasidan foydalaning.
Karta raqamlari — misol uchun (8600 1234 5678 9012 — soxta).
Yechim
import { Controller, useForm } from "react-hook-form";
const toCardDigits = (text) => text.replace(/\D/g, "").slice(0, 16);
const formatCard = (d) => d.replace(/(\d{4})(?=\d)/g, "$1 ");
export default function App() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({ defaultValues: { card: "" } });
return (
<form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
<label htmlFor="card">Karta raqami</label>
<Controller
control={control}
name="card"
rules={{
validate: (v) =>
v.length === 16 || "Karta raqami 16 ta raqam",
}}
render={({ field }) => (
<input
id="card"
ref={field.ref}
inputMode="numeric"
autoComplete="cc-number"
value={formatCard(field.value)}
onChange={(e) =>
field.onChange(toCardDigits(e.target.value))
}
onBlur={field.onBlur}
/>
)}
/>
<p role="alert">{errors.card?.message}</p>
<button>To'lash</button>
</form>
);
}Konsolda:
{card: '8600123456789012'}formatCard — "to'rtta raqamdan keyin, agar yana raqam bo'lsa, bo'shliq". autoComplete="cc-number" — brauzer saqlangan kartani taklif qiladi. Controller ning rules — register dagi qoidalar bilan bir xil (RHF I). Haqiqiy to'lovda karta raqami sizning serveringizga umuman kelmasligi kerak — to'lov tizimining (Click, Payme) o'z sahifasi yoki vidjeti uni oladi (29-qism).
3-mashq (qiyin): taom combobox'i — RHF bilan
«Kichik combobox» bo'limidagi TagCombobox ni buyurtma formasiga ulang:
- Takliflar — menyu:
["Osh", "Manti", "Lag'mon", "Mastava"], kichik harf bilan qidiriladi ("ma"→ Manti, Mastava). dishmaydoni —Controllerorqali;refvaonBlurham ulansin (komponent ularni qabul qilsin).- Majburiy: "Taomni tanlang yoki yozing" (
rules). - Yuborishda —
{dish: …}konsolga.
Ishora: onBlur ni komponentning o'z onBlur ishlovchisi ichida chaqiring — ro'yxatni yopgandan keyin.
Yechim
import { useId, useState } from "react";
import { Controller, useForm } from "react-hook-form";
const MENU = ["Osh", "Manti", "Lag'mon", "Mastava"];
function DishCombobox({ value, onChange, onBlur, ref }) {
const listId = useId();
const [open, setOpen] = useState(false);
const query = value.toLowerCase();
const matches = MENU.filter(
(d) => d.toLowerCase().startsWith(query) && d !== value,
);
const shown = open && matches.length > 0;
return (
<span>
<label htmlFor="dish">Taom</label>
<input
id="dish"
ref={ref}
role="combobox"
aria-autocomplete="list"
aria-expanded={shown}
aria-controls={listId}
autoComplete="off"
value={value}
onChange={(e) => {
onChange(e.target.value);
setOpen(true);
}}
onBlur={() => {
setOpen(false);
onBlur();
}}
/>
<ul id={listId} role="listbox" hidden={!shown}>
{matches.map((d) => (
<li
key={d}
role="option"
aria-selected={false}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
onChange(d);
setOpen(false);
}}
>
{d}
</li>
))}
</ul>
</span>
);
}
export default function App() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({ defaultValues: { dish: "" } });
return (
<form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
<Controller
control={control}
name="dish"
rules={{ required: "Taomni tanlang yoki yozing" }}
render={({ field }) => (
<DishCombobox
ref={field.ref}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
/>
<p role="alert">{errors.dish?.message}</p>
<button>Buyurtma</button>
</form>
);
}Konsolda:
{dish: 'Manti'}Ekranda:
Taom
BuyurtmaBo'sh yuborildi — xato; ma yozildi — Manti va Mastava; Manti bosildi — xato yo'qoldi (birinchi yuborishdan keyin reValidateMode: "onChange"), yuborildi. Klaviatura (↓ ↑ Enter Esc) — TagCombobox dagidek qo'shing; bu yechimda qisqalik uchun faqat sichqoncha.
4-mashq: Vazifalar qadami
feat/teg-combobox branch'ida ui/Combobox.tsx (testi bilan), YangiLoyiha.tsx dagi Controller va takliflar, CSS. Commit.
Yechim
Tekshiruv ro'yxati:
npm test— node:test 195 («Orqaga» tuzatishigacha — 187), Vitest 67.npm run dev→/yangi: tegga#byozing — ro'yxatda#bozor; ↓ — u faol (rangli), fokus maydonda; Enter — tanlandi, qadam o'zgarmadi. Yana Enter (taklif yo'q) — "Keyingi" ishlaydi.- DevTools Accessibility panelida maydon:
combobox,expanded: true,activedescendant— variant id'si. git log --oneline -1—feat: teg combobox (ARIA 1.2) — ko'p qadamli formada.
11. Real ishda
- Telefon va karta maskalari — har o'zbek internet-do'koni, yetkazib berish va bank ilovasida. Odatda
react-imaskyokireact-number-format. - OTP — Click, Payme, Uzum kabi ilovalarda kirish va to'lov tasdig'i. Shadcn/ui
InputOTP(input-otp) keng tarqalgan. - Combobox — manzil qidirish, mahsulot qidirish, teg va kategoriya tanlash. A11y talablari kuchli bo'lgan joyda (davlat xizmatlari, bank) — Downshift, React Aria yoki Radix.
- Intervyu savollari: "maskali maydonda formaga qaysi qiymat ketadi?", "nega telefon uchun
type=numberemas?", "combobox'da fokus qayerda bo'ladi va nega?", "aria-activedescendantnima?".
Xulosa
- Maxsus input: formada xom qiymat (raqamlar, son,
"YYYY-MM-DD"), ekranda — formatlangan; ulanishControllerbilan. - Telefon:
type="tel",inputMode="tel", maska; kursor uchun real loyihadareact-imask. So'm: o'z guruhlash yokireact-number-format;Intlnatijasi muhitga qarab farq qiladi. - OTP: bitta maydon,
autoComplete="one-time-code",inputMode="numeric"; katakchalar kerak bo'lsainput-otp. - Sana: avval
<input type="date">;new Date("YYYY-MM-DD")— UTC tuzog'i; murakkab kalendar —react-day-picker. - Combobox: ARIA 1.2 — fokus maydonda,
aria-activedescendant,onMouseDown+preventDefault;vazifalarda teg combobox'i, telefon/sana/OTP esa ma'lumot modeli o'zgarmasligi uchun yo'q.
Keyingi dars: Fayl yuklash UI — fayl tanlash va sudrab tashlash, oldindan ko'rish, progress va bekor qilish; vazifalar ga profil rasmi va 19-qism yakuni (v9).
Manbalar
- WAI-ARIA APG: Combobox pattern — w3.org/WAI/ARIA/apg/patterns/combobox (ARIA 1.2)
- MDN:
inputmode,autocomplete(one-time-code,tel,cc-number) — developer.mozilla.org/docs/Web/HTML/Attributes/autocomplete - MDN:
<input type="date">, WebOTP API — developer.mozilla.org/docs/Web/API/WebOTP_API - IMask — imask.js.org; react-number-format — s-yadav.github.io/react-number-format; input-otp — github.com/guilhermerodz/input-otp; react-day-picker — daypicker.dev; Downshift — downshift-js.com
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!