IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya3/30-dars30 daqiqa
Mundarija (36)

shadcn/ui komponentlari real ilovada: DropdownMenu, Dialog, Sheet, Popover, Form va DataTable

Qisqacha: Admin panel (dashboard) bir xil "g'ishtlar"dan yig'iladi. Bular: Sidebar, DropdownMenu, Dialog, Sheet, Popover, Command, Field (forma), DataTable va Chart. shadcn/ui ularning har birini loyihangizga fayl qilib ko'chiradi. Ostida xulqni Radix beradi, ko'rinishni Tailwind sinflari. Eng ko'p ishlatiladigani — DropdownMenu: Root → Trigger → Portal → Content → Item. Band tanlansa onSelect chaqiriladi va menyu o'zi yopiladi.

Bu darsda

  • Dashboard'ni shadcn/ui komponentlariga bo'lasiz va har birining ostida nima turganini ayta olasiz.
  • Radix DropdownMenu'ni qismlari bilan yozasiz: Trigger, Portal, Content, Item, onSelect.
  • shadcn'ning dropdown-menu.tsx fayli primitivni qanday "o'rashini" tushunasiz va o'zingiz shunday o'ram yozasiz.
  • Belgilash (checkbox), tanlash (radio), ajratgich va sarlavhali menyu qurasiz.
  • vazifalar dagi o'z Menyu mizni shadcn DropdownMenu bilan almashtirasiz va narxini (+27.2 KB) o'lchab ko'rasiz.

Oldin bilishingiz kerak: shadcn/ui I: qanday ishlaydi va sozlash, Compound komponentlar va slot naqshi, Headless komponentlar va prop getters, Portallar, RHF II: Controller, watch va tahrirlash formasi.

1. Nega bu kerak?

Oldingi darsda shadcn/ui'ni sozladik: components.json, @/ alias, cn() va cva. Loyiha komponent qabul qilishga tayyor. Endi savol: qaysi komponentlar va ular real ilovada qanday birlashadi?

Jasur aka «Bahor» uchun admin panel so'radi. Chapda bo'limlar: buyurtmalar, menyu, xodimlar. O'rtada buyurtmalar jadvali, har qator oxirida «Amallar» tugmasi: ko'rish, tahrirlash, o'chirish. O'chirishdan oldin "Ishonchingiz komilmi?" oynasi. Tepada filtr va "yangi taom" formasi. Burchakda kunlik savdo grafigi.

Bularni noldan yozsak-chi? 18-qismda bitta ochiladigan menyuni o'zimiz yozgan edik: Compound komponentlar, Controlled va uncontrolled API va Headless komponentlar darslari — uch dars, 326 qator kod. U ishladi, lekin bitta menyu, xolos. Dashboard'da esa o'nga yaqin shunday komponent bor.

Hayotiy o'xshatish: yangi oshxona jihozlash. Har bir pichoq va qozonni temirchiga buyurtma qilish mumkin. Lekin aqlli oshpaz sinalgan to'plamni oladi, keyin qaysidir pichoqni o'z qo'liga moslab charxlaydi. shadcn/ui — ana shu to'plam. Farqi: pichoqlar sizga berib yuboriladi, ijaraga emas. Fayl sizniki, xohlaganingizcha o'zgartirasiz.

2. Dashboard xaritasi

2.1 Qaysi qism — qaysi komponent

Avval katta rasmni ko'raylik. Diagrammada «Bahor» admin paneli qismlari va ularning ostidagi shadcn komponentlari:

flowchart TB
  P["Admin panel"] --> S["Sidebar: bo'limlar"]
  P --> T["DataTable: buyurtmalar"]
  P --> C["Chart: savdo"]
  T --> D["DropdownMenu: qator amallari"]
  D --> A["Dialog: o'chirishni tasdiqlash"]
  D --> SH["Sheet: tahrirlash formasi"]
  SH --> F["Field + React Hook Form"]
  T --> PO["Popover: filtr"]
  P --> K["Command: Ctrl+K qidiruv"]

Diagrammaga qarang: komponentlar bir-birining ichida yashaydi. Qator menyusi tasdiq oynasini ochadi, yon oyna ichida forma turadi. Shuning uchun ular bitta oiladan bo'lgani qulay. Har birining ostida nima turadi:

Komponent Vazifasi Ostida To'liq dars
DropdownMenu bosilganda ochiladigan amallar ro'yxati Radix Menu shu dars, Radix chuqur
Dialog, AlertDialog e'tiborni to'liq oladigan oyna Radix Dialog Modal, drawer, popover va tooltip
Sheet ekran chetidan chiqadigan panel Radix Dialog Modal, drawer…
Popover tugma yonida ochiladigan kichik panel Radix Popover Modal, drawer…
Tooltip ustiga kelganda qisqa izoh Radix Tooltip Modal, drawer…
Komponent Vazifasi Ostida To'liq dars
Command qidiruvli buyruqlar ro'yxati cmdk Command palette
Field (forma) yorliq, maydon, xato matni React Hook Form RHF II
DataTable saralash, filtr, sahifalash TanStack Table Jadvallar I
Chart ustun, chiziq, doira grafiklar Recharts shu dars (qisqa)
Sidebar yig'iladigan yon panel o'z kodi shu dars (qisqa)

Bu darsda DropdownMenu'ni to'liq ochamiz — vazifalar dagi bugungi qadam aynan u. Qolganlari bilan qisqa tanishamiz.

2.2 Bloklar: butun sahifa bir buyruqda

shadcn'da komponentdan kattaroq birlik ham bor — blok (block). Blok — bir nechta komponentdan yig'ilgan tayyor sahifa yoki bo'lim. Masalan dashboard-01 — "yon panel, grafiklar va jadvalli dashboard". sidebar-01 dan sidebar-16 gacha — yon panelning 16 xil varianti, login-01 kabi kirish sahifalari ham bor.

bash
npx shadcn@4.21.4 add dashboard-01

Buyruq o'nlab faylni loyihangizga yozadi: sahifa, yon panel, jadval, grafik va ular ishlatadigan har bir komponent. Oldingi darsdagi qoida bu yerda ikki karra muhim. CLI qo'shgan har bir paketni va versiyasini tekshiring, ^ belgisini olib tashlang. Blok — boshlang'ich nuqta, tayyor mahsulot emas: ichidagi namuna ma'lumotlarni o'zingiznikiga almashtirasiz.

Maslahat: Blokni avval alohida bo'sh loyihada add qiling va nima kelishini ko'ring. Keyin keragini qo'lda ko'chiring — kanondagi dropdown-menu.tsx shunday ko'chirilgan.

Tekshirib ko'ring: Sheet bilan Dialog'ning ostida bir xil primitiv turibdi. Nega ular ikki xil komponent?

Javob

Xulq bir xil: ochilganda fokus ichkariga kiradi, Esc yopadi, orqa fon bosilmaydi. Farq — ko'rinishda va maqsadda. Dialog ekran o'rtasida qisqa savol beradi ("o'chirilsinmi?"). Sheet ekran chetidan chiqadi va ichida uzunroq ish bo'ladi (tahrirlash formasi, filtrlar). shadcn bitta Radix Dialog ustiga ikki xil o'ram yozgan — sheet.tsx da side props'i panelni qaysi chetdan chiqarishni aytadi.

3. DropdownMenu: amallar menyusi

3.1 Beshta qism

Ochiladigan menyu (dropdown menu) — tugma bosilganda paydo bo'ladigan amallar ro'yxati. Ro'yxatdan bitta amal tanlanadi va menyu yopiladi. Telefoningizdagi Telegram'da xabarni bosib turganda chiqadigan "Javob berish, Nusxalash, O'chirish" ro'yxati — xuddi shu.

Radix uni besh qismga bo'ladi. Bu Compound komponentlar darsidagi "Radix uslubidagi anatomiya"ning o'zi:

Qism Nima qiladi
Root holatni saqlaydi (ochiq/yopiq), qismlarni Context bilan bog'laydi
Trigger menyuni ochadigan tugma
Portal ro'yxatni <body> oxiriga chizadi
Content ro'yxatning o'zi: joylashuv, klaviatura
Item bitta amal

Bu qismlar Radix'ning primitivlari (primitives) deyiladi. Primitiv — stilsiz, lekin to'liq xulqli qurilish bo'lagi: klaviatura, fokus, ekran o'quvchi uchun atributlar — hammasi bor, rang va o'lcham yo'q.

Bir eslatma. Saytdagi natija oynasida Tailwind ishlamaydi — u yerda build bosqichi yo'q. Shuning uchun jonli misollarda oddiy CSS yozamiz. Loyihada xuddi shu narsa Tailwind sinflari bilan yoziladi.

«Bahor» buyurtmasi qatoridagi amallar menyusi:

jsx
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

const css = `
  .menu { min-width: 10rem; padding: 4px; background: #fff;
    border: 1px solid #bbb; border-radius: 8px; color: #111; }
  .item { padding: 8px 12px; border-radius: 6px; outline: none; }
  .item[data-highlighted] { background: #1d4ed8; color: #fff; }
`;

export default function App() {
  const [action, setAction] = useState("—");
  return (
    <div>
      <style>{css}</style>
      <p>Buyurtma №12 — 63 000 so'm</p>
      <DropdownMenu.Root modal={false}>
        <DropdownMenu.Trigger>Amallar</DropdownMenu.Trigger>
        <DropdownMenu.Portal>
          <DropdownMenu.Content className="menu" align="start">
            <DropdownMenu.Item
              className="item"
              onSelect={() => setAction("Ko'rish")}
            >
              Ko'rish
            </DropdownMenu.Item>
            <DropdownMenu.Item
              className="item"
              onSelect={() => setAction("Tahrirlash")}
            >
              Tahrirlash
            </DropdownMenu.Item>
            <DropdownMenu.Item
              className="item"
              onSelect={() => setAction("O'chirish")}
            >
              O'chirish
            </DropdownMenu.Item>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>
      <p>Tanlangan amal: {action}</p>
    </div>
  );
}

Ekranda («Amallar» → ↓ → ↓ → Enter):

text
Buyurtma №12 — 63 000 so'm
Amallar
Tanlangan amal: Tahrirlash

«Ishga tushir» ni bosing, keyin «Amallar» ni. Ro'yxat ochiladi. Endi sichqonchani qo'yib, klaviaturani sinang: ↓ birinchi bandga, yana ↓ — «Tahrirlash» ga o'tadi, Enter tanlaydi. Menyu yopiladi, pastda amal nomi chiqadi. ↑ orqaga yuradi, Esc tanlovsiz yopadi. Bularning birortasini biz yozmadik.

Qadam Fokus Ekranda
«Amallar» bosildi ro'yxatning o'zida ro'yxat ochildi
↓ «Ko'rish» band ajratildi
↓ «Tahrirlash» band ajratildi
Enter «Amallar» tugmasiga qaytdi ro'yxat yopildi, «Tanlangan amal: Tahrirlash»

Kodni qatorma-qator o'qiymiz:

  1. onSelect — band tanlanganda chaqiriladi: sichqoncha bilan bosilsa ham, Enter yoki Space bosilsa ham. Tanlovdan keyin menyu o'zi yopiladi va fokus «Amallar» tugmasiga qaytadi.
  2. data-highlighted — Radix hozir "ustida turgan" bandga qo'yadigan atribut. Sichqoncha bandga kelsa, fokus ham o'sha bandga o'tadi. Shuning uchun bitta qoida ikkalasini bezaydi, :hover alohida kerak emas.
  3. modal={false} — menyu ochiq turganda sahifaning qolgan qismi ham ishlayveradi. Nega bunday qilganimizni Radix chuqur darsida o'lchov bilan ko'ramiz. Hozircha shuni biling: Radix'ning standart qiymati — true.
  4. align="start" — ro'yxat tugmaning chap chetiga tekislanadi. Standart — center.

Ochiq yoki yopiqlikni biz saqlamadik — useState yo'q, menyuni Radix o'zi boshqaradi. Bu 17-qismdagi uncontrolled rejim: defaultOpen bilan faqat boshlang'ich holatni berish mumkin ("boshida ochiq bo'lsin"), xuddi defaultValue kabi. Controlled rejim (open + onOpenChange) — keyingi darsda.

Diqqat: Ekranda ro'yxat tugma yonida, lekin DOM'da (DevTools Elements) u <body> oxirida turadi. Bu Portal ning ishi. Joyini esa Floating UI hisoblaydi. Ota-elementning overflow: hidden i yoki z-index i ro'yxatni endi kesmaydi — Portallar darsidagi "qamoq" muammosi shunday yechiladi.

3.2 shadcn qatlami: primitiv ustidagi o'ram

Har safar DropdownMenu.Portal, className="menu", align="start" yozish — takrorlash. shadcn shu takrorni bir faylga yig'adi. npx shadcn add dropdown-menu yozgan dropdown-menu.tsx aslida bir necha o'ram (wrapper) funksiyadan iborat. Har biri bitta Radix qismini oladi va unga loyihaning standartlarini qo'shadi.

Shunday o'ramni o'zimiz yozamiz. cn o'rnida clsx — oldingi darsdagi cn() ning asosi:

jsx
import { useState } from "react";
import { clsx } from "clsx";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";

// --- ui/dropdown-menu.jsx: bir marta yoziladi ---
function DropdownMenu({ modal = false, ...props }) {
  return <DropdownMenuPrimitive.Root modal={modal} {...props} />;
}

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;

function DropdownMenuContent({ className, ...props }) {
  return (
    <DropdownMenuPrimitive.Portal>
      <DropdownMenuPrimitive.Content
        data-slot="dropdown-menu-content"
        align="start"
        sideOffset={4}
        className={clsx("menu", className)}
        {...props}
      />
    </DropdownMenuPrimitive.Portal>
  );
}

function DropdownMenuItem({
  variant = "default",
  className,
  ...props
}) {
  return (
    <DropdownMenuPrimitive.Item
      data-slot="dropdown-menu-item"
      data-variant={variant}
      className={clsx("item", className)}
      {...props}
    />
  );
}

// --- ilova: endi qisqa ---
const css = `
  .menu { padding: 4px; background: #fff; color: #111;
    border: 1px solid #bbb; border-radius: 8px; }
  .item { padding: 8px 12px; outline: none; }
  .item[data-highlighted] { background: #1d4ed8; color: #fff; }
  .item[data-variant="destructive"] { color: #b91c1c; }
`;

export default function App() {
  const [log, setLog] = useState("—");
  return (
    <div>
      <style>{css}</style>
      <DropdownMenu>
        <DropdownMenuTrigger>Taom: Osh</DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem onSelect={() => setLog("Arxivlash")}>
            Arxivlash
          </DropdownMenuItem>
          <DropdownMenuItem
            variant="destructive"
            onSelect={() => setLog("O'chirish")}
          >
            O'chirish
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <p>Oxirgi amal: {log}</p>
    </div>
  );
}

Ekranda:

text
Taom: Osh
Oxirgi amal: Arxivlash

Pastki qismga qarang: ilova kodi endi Portal, joylashuv va sinflar haqida o'ylamaydi — ular o'ramga ko'chdi. shadcn fayli aynan shunday tuzilgan:

  • Har qism — alohida funksiya, nomi hujjatdagidek (DropdownMenuItem). Hujjat misollari loyihangizda o'zgarishsiz ishlaydi.
  • data-slot — "bu qaysi qism" belgisi. CSS'da aniq qismni topish oson: [data-slot=dropdown-menu-item].
  • variant — shadcn qo'shimchasi, Radix'da yo'q. "destructive" (xavfli amal) data-variant atributiga aylanadi, stil atributga qarab beriladi.
  • ...props oxirida — chaqiruvchi har standartni bosib keta oladi (Komponent API'sini loyihalash qoidasi).

Maslahat: shadcn'ning to'liq dropdown-menu.tsx faylida 15 ta funksiya bor (Sub, Shortcut, CheckboxItem…). Kanon faqat 4 tasini qoldirgan — ishlatilmagan eksportni knip xato deydi (knip darsida). Fayl sizniki: keragini qoldiring, kerak bo'lsa yana CLI'dan oling.

3.3 Bandlar turlari: belgilash, tanlash, ajratgich

Menyu faqat "amal" emas. Ro'yxat sahifasida «Ko'rinish» menyusi bo'ladi: "faqat bajarilganlar" — belgilanadi yoki yo'q; tartib — "yangi" yoki "alifbo" dan biri. Buning uchun Radix'da maxsus bandlar bor:

Qism Nima ARIA roli
CheckboxItem yoqiladi/o'chiriladi (checked) menuitemcheckbox
RadioGroup + RadioItem bir nechtadan bittasi (value) menuitemradio
ItemIndicator belgi: yoki ● — faqat tanlanganda chiziladi —
Label, Separator, Group sarlavha, chiziq, guruh —
jsx
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

const css = `
  .menu { padding: 4px; background: #fff; color: #111;
    border: 1px solid #bbb; border-radius: 8px; }
  .item { padding: 6px 12px; outline: none; }
  .item[data-highlighted] { background: #1d4ed8; color: #fff; }
  .label { padding: 6px 12px; font-size: 12px; color: #555; }
  .sep { height: 1px; margin: 4px 0; background: #ddd; }
`;

export default function App() {
  const [onlyDone, setOnlyDone] = useState(false);
  const [order, setOrder] = useState("yangi");
  return (
    <div>
      <style>{css}</style>
      <DropdownMenu.Root modal={false}>
        <DropdownMenu.Trigger>Ko'rinish</DropdownMenu.Trigger>
        <DropdownMenu.Portal>
          <DropdownMenu.Content className="menu" align="start">
            <DropdownMenu.Label className="label">
              Filtr
            </DropdownMenu.Label>
            <DropdownMenu.CheckboxItem
              className="item"
              checked={onlyDone}
              onCheckedChange={setOnlyDone}
              onSelect={(e) => e.preventDefault()}
            >
              <DropdownMenu.ItemIndicator>
                ✓{" "}
              </DropdownMenu.ItemIndicator>
              Faqat bajarilganlar
            </DropdownMenu.CheckboxItem>
            <DropdownMenu.Separator className="sep" />
            <DropdownMenu.Label className="label">
              Tartib
            </DropdownMenu.Label>
            <DropdownMenu.RadioGroup
              value={order}
              onValueChange={setOrder}
            >
              <DropdownMenu.RadioItem className="item" value="yangi">
                <DropdownMenu.ItemIndicator>
                  ●{" "}
                </DropdownMenu.ItemIndicator>
                Yangi
              </DropdownMenu.RadioItem>
              <DropdownMenu.RadioItem className="item" value="alifbo">
                <DropdownMenu.ItemIndicator>
                  ●{" "}
                </DropdownMenu.ItemIndicator>
                Alifbo
              </DropdownMenu.RadioItem>
            </DropdownMenu.RadioGroup>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>
      <p>
        Filtr: {onlyDone ? "bajarilganlar" : "hammasi"}, tartib:{" "}
        {order}
      </p>
    </div>
  );
}

Ekranda («Ko'rinish» → «Faqat bajarilganlar» → «Alifbo»):

text
Ko'rinish
Filtr: bajarilganlar, tartib: alifbo

«Faqat bajarilganlar» bosilganda menyu yopilmadi. Sabab — onSelect={(e) => e.preventDefault()}: preventDefault() "standart ishni qilma" deydi, bu yerda standart ish — yopish. Belgilash bandlarida bu qulay: odam ketma-ket bir nechtasini belgilaydi. «Alifbo» esa oddiy band kabi ishladi: tanlandi, menyu yopildi.

Holat (onlyDone, order) bizning useState da. Radix uni ko'rsatadi va o'zgarishini xabar qiladi: checked + onCheckedChange, value + onValueChange. Shu holatni saralash va filtrga bersangiz — tayyor «Ko'rinish» menyusi.

Yana ikki tur bor. Sub — ichma-ich menyu (Sub → SubTrigger → SubContent): «Eksport» ustida "CSV, JSON" ochiladi, → va ← kiradi va chiqadi. shadcn'dagi DropdownMenuShortcut esa shunchaki <span> ("E" yozuvi) — yorliqni o'zi ishlatmaydi (Command palette va klaviatura yorliqlari).

Tekshirib ko'ring: Belgilash bandidan onSelect={(e) => e.preventDefault()} olib tashlansa, «Faqat bajarilganlar» bosilganda nima bo'ladi? Belgi qo'yiladimi?

Javob

Belgi qo'yiladi — onCheckedChange baribir chaqiriladi va onlyDone true bo'ladi. Lekin menyu yopiladi, chunki tanlovdan keyingi standart ish — yopish. Ikkinchi filtrni belgilash uchun odam menyuni qayta ochishi kerak. Bitta belgi bo'lsa — farqi kam. Beshta filtrli menyuda esa odam besh marta ochib-yopadi.

3.4 Bepul keladigan klaviatura va a11y

Headless komponentlar darsida useMenyu hookini yozganmiz: strelkalar, Home/End, harf bilan qidirish, Esc, tashqariga bosish. Radix'da hammasi bor va bundan ham ko'p:

Klaviatura Natija
Enter, Space yoki ↓ (tugmada) ochiladi, fokus birinchi bandda
↓ ↑ keyingi / oldingi band; loop bo'lsa oxiridan boshiga
Home / End birinchi / oxirgi band
harf shu harf bilan boshlangan band
Esc yopiladi, fokus tugmaga qaytadi
Tab hech narsa: fokus bandda, menyu ochiq (sababi va APG bilan farqi — keyingi darsda)

disabled band strelkada o'tkazib yuboriladi. Malika (ekran o'quvchi) uchun aria-haspopup, aria-expanded, aria-controls, role="menu" va role="menuitem" ni Radix o'zi qo'yadi. Fokus qayerga borishini keyingi darsda tekshiramiz.

4. Dashboard'ning boshqa g'ishtlari

Har biri bilan qisqa tanishamiz. Maqsad — "qachon qaysi biri" ni bilish. Chuqur o'rganish — jadvaldagi darslarda.

4.1 Dialog va AlertDialog

Dialog — sahifaning ustiga chiqadigan va e'tiborni to'liq oladigan oyna. Ochiq turganda orqadagi sahifa bosilmaydi, fokus oyna ichida aylanadi. «Bahor» da taomni o'chirishdan oldin so'rash:

jsx
import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";

export default function App() {
  const [deleted, setDeleted] = useState(false);
  if (deleted) return <p>Osh menyudan olib tashlandi.</p>;
  return (
    <Dialog.Root>
      <Dialog.Trigger>Osh'ni o'chirish</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay />
        <Dialog.Content>
          <Dialog.Title>Ishonchingiz komilmi?</Dialog.Title>
          <Dialog.Description>
            Osh menyudan olib tashlanadi.
          </Dialog.Description>
          <Dialog.Close>Bekor qilish</Dialog.Close>{" "}
          <button onClick={() => setDeleted(true)}>
            Ha, o'chirish
          </button>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

Ekranda:

text
Osh menyudan olib tashlandi.

Anatomiya tanish: Root, Trigger, Portal, Content. Yangilari — Overlay (orqa fonni qoplovchi qatlam), Title va Description (oyna nomi va izohi — ekran o'quvchi ochilganda shularni o'qiydi), Close (yopuvchi tugma). shadcn'da AlertDialog ham bor — xavfli amalni tasdiqlash uchun. Farqi: orqa fonni bosish uni yopmaydi, odam aniq javob berishi shart.

Bir halol eslatma. vazifalar dagi o'chirish tasdig'i Radix Dialog emas — native <dialog> (<dialog> va Popover API JS'dan darsida yozganmiz). Kanon Radix AlertDialog + sonner variantini o'lchadi: ilovaning xavfsizlik siyosati (CSP) ostida 4 ta buzilish. Sabab va o'lchov — Radix chuqur darsida.

4.2 Sheet — chetdan chiqadigan panel

Sheet — yuqorida aytganimizdek, Dialog'ning boshqa ko'rinishi. Jadvaldagi buyurtmani tahrirlash uchun o'ng chetdan forma chiqadi, jadval esa orqada ko'rinib turadi. shadcn'da shunday yoziladi (loyihada, Tailwind bilan):

tsx
<Sheet>
  <SheetTrigger asChild>
    <Button variant="outline">Tahrirlash</Button>
  </SheetTrigger>
  <SheetContent side="right">
    <SheetHeader>
      <SheetTitle>Buyurtma №12</SheetTitle>
      <SheetDescription>O'zgartiring va saqlang.</SheetDescription>
    </SheetHeader>
    <OrderForm />
  </SheetContent>
</Sheet>

side — top, right, bottom yoki left. asChild — Trigger o'z tugmasini chizmasin, bizning Button ni ishlatsin (Polymorphic komponentlar va asChild; Radix'da — keyingi darsda). Telefondagi pastki "tortma" uchun — Drawer (vaul kutubxonasi).

4.3 Popover va Tooltip

Popover — tugma yonida ochiladigan kichik panel. Ichida istalgan narsa bo'lishi mumkin: filtr, sana tanlash, rang tanlash. Menyudan farqi: Popover ichidagi narsalar amal emas, oddiy boshqaruv elementlari. Tab bilan ular bo'ylab yuriladi, strelkalar bilan emas.

jsx
import * as Popover from "@radix-ui/react-popover";

export default function App() {
  return (
    <Popover.Root>
      <Popover.Trigger>Filtr</Popover.Trigger>
      <Popover.Portal>
        <Popover.Content sideOffset={4}>
          <label>
            <input type="checkbox" /> Faqat yetkazib berish
          </label>{" "}
          <Popover.Close>Yopish</Popover.Close>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}

Ekranda:

text
Filtr
Faqat yetkazib berish Yopish

Bu yerda bos("Filtr") oddiy bosish bilan ochdi. Popover tugmasi click hodisasida ochiladi. DropdownMenu tugmasi esa pointerdown da — sichqoncha tugmasi bosila boshlaganda, qo'yib yuborilishini kutmay. Sababi va testlardagi oqibati — «Ko'p uchraydigan xatolar» bo'limida.

Tooltip — ustiga kelganda yoki fokusda chiqadigan qisqa izoh, ikonka tugmalar uchun ("" nima qiladi?). Ichida tugma yoki havola bo'lmaydi, faqat matn. Radix talabi: hamma tooltiplar Tooltip.Provider ichida (u kechikishni boshqaradi). Unutilsa nima bo'lishi — xatolar bo'limida.

4.4 Command — buyruqlar oynasi

Command — qidiruv maydoni va filtrlangan ro'yxat: yozasiz, ro'yxat qisqaradi, ↓ va Enter bilan tanlaysiz. VS Code'dagi Ctrl+Shift+P ni eslang. shadcn'da u cmdk kutubxonasi ustiga qurilgan, Dialog ichida ochilsa — CommandDialog. vazifalar da u Ctrl+K bilan ochiladi va faqat birinchi ochilganda yuklanadi — Command palette va klaviatura yorliqlari darsida.

4.5 Field — forma

Formalar uchun shadcn'ning yangi yo'li — Field komponentlari (2025-yil oktabrda qo'shilgan): Field, FieldLabel, FieldDescription, FieldError. Ular ma'lumotni o'zi boshqarmaydi — React Hook Form'ning Controller i bilan ishlaydi. RHF II darsidagi Controller ni eslang:

tsx
<Controller
  name="name"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field data-invalid={fieldState.invalid}>
      <FieldLabel htmlFor={field.name}>Taom nomi</FieldLabel>
      <Input
        {...field}
        id={field.name}
        aria-invalid={fieldState.invalid}
      />
      {fieldState.invalid && (
        <FieldError errors={[fieldState.error]} />
      )}
    </Field>
  )}
/>

Hammasi tanish: aria-invalid — React'da accessibility darsidagi xato belgisi, htmlFor — yorliq bog'lanishi. data-invalid — stil uchun: yorliq va chegara qizil bo'ladi. Eski kodda <Form>, <FormField>, <FormMessage> ni ko'rasiz — shadcn'ning oldingi Form komponenti, u ham RHF ustida: FormField ≈ Controller, FormMessage ≈ FieldError.

4.6 DataTable va Chart

DataTable — shadcn'da komponent emas, qo'llanma. U TanStack Table ustida jadval yig'ishni bosqichma-bosqich ko'rsatadi: ustunlar, saralash, filtr, sahifalash, qator tanlash. Bitta tuzoq: shadcn qo'llanmasi TanStack Table v8 API'sida (useReactTable). Kanonda v9 (useTable) — Jadvallar I darsida. Qo'llanmani ko'chirsangiz, versiyani tekshiring.

Chart — Recharts grafiklari ustidagi ingichka qatlam: ChartContainer, ChartTooltip, ChartLegend. Ranglar CSS o'zgaruvchilarida (--color-savdo), qorong'i temada o'zi almashadi. Kanonda Chart yo'q: teg ulushlari oddiy CSS chiziqlari (Kutubxona turlari va module augmentation darsida). Yana bir sabab: chart.tsx dagi ChartStyle sahifaga <style> tegini o'zi yozadi, kanonning xavfsizlik siyosati esa bunday teglarni to'sadi. Chart'ni alohida o'lchamadik — sabab Radix menyuniki bilan bir xil, keyingi darsda ko'ramiz.

4.7 Sidebar

Sidebar — yig'iladigan yon panel: SidebarProvider (holat; telefonda Sheet'ga aylanadi), Sidebar, SidebarHeader, SidebarContent, SidebarGroup, SidebarFooter va SidebarTrigger. Yana compound naqsh. Odatda u blok bilan olinadi (sidebar-07 — ikonkalarga yig'iladigan panel). vazifalar da yon panel yo'q: sahifalar menyusi tepada (React Router I).

Tekshirib ko'ring: Uchta vaziyat: (a) jadval qatoridagi "…" tugmasi; (b) "Faqat yetkazib berish" belgisi va narx oralig'i surgichi; (c) "Hisobni o'chirasizmi?" Har biriga qaysi komponent?

Javob

(a) DropdownMenu — amallar ro'yxati, bittasi tanlanadi va menyu yopiladi. (b) Popover — ichida oddiy boshqaruv elementlari (checkbox, surgich), Tab bilan yuriladi; menyu bo'lsa, surgichni strelkalar bilan boshqarib bo'lmasdi — strelkalar bandlar bo'ylab yuradi. (c) AlertDialog — xavfli amal: orqa fonni bosish yopmaydi, odam aniq "Ha" yoki "Yo'q" deyishi kerak.

5. Vazifalar qadami: menyular — shadcn DropdownMenu (holat 03)

5.1 Nima o'zgaradi

vazifalar da ikkita menyu bor: statistika panelidagi «Hisobotni yuklab olish» (CSV yoki JSON) va tepada foydalanuvchi menyusi («Chiqish»). Ikkalasi 18-qismda yozilgan o'z Menyu mizda: Menyu.tsx (100 qator), useMenyu.ts (200) va useBoshqariladiganHolat.ts (26). Bugun ular shadcn DropdownMenu bilan almashadi, uchala fayl o'chiriladi.

Nega ishlab turgan kodni almashtiramiz? Ilovaga tooltip, buyruqlar oynasi va boshqa komponentlar kelyapti — har biriga klaviatura, fokus va ARIA'ni o'zimiz yozsak, dizayn tizimini noldan quramiz. Narxi ham bor, uni quyida o'lchaymiz.

bash
git switch -c feat/dropdown-menu
npm i -E @radix-ui/react-dropdown-menu@2.1.25

-E (--save-exact) — aniq versiya, ^ siz. CLI shadcn'ning umumiy radix-ui paketini qo'shardi. Kanon esa alohida @radix-ui/react-dropdown-menu ni oldi. Sababi: umumiy paket bilan tsc -b xato berdi, chunki uning turlari hamma primitivni tortadi, react-select ni ham (kanon o'lchovi):

text
node_modules/@radix-ui/react-select/dist/index.d.mts(108,11): error TS2320: Interface 'SelectPopperPositionProps' cannot simultaneously extend types 'Omit<PopperContentProps & RefAttributes<HTMLDivElement>, "ref">' and 'SelectPopperPrivateProps'.

Tarjimasi: "SelectPopperPositionProps interfeysi ikki turni bir vaqtda kengaytira olmaydi" — Select'ning turlarida ichki ziddiyat. Biz Select ishlatmaymiz-ku, lekin tsc umumiy paketning hamma turlarini tekshiradi. Alohida paketda bu muammo yo'q, hajm esa bir xil: build baribir faqat ishlatilgan kodni oladi (tree shaking).

5.2 ui/dropdown-menu.tsx

CLI yozgan fayl (266 qator, 15 funksiya) qo'lda ko'chirildi va moslandi. Kod kanondan aynan; izohlar qisqartirilgan va telefonga sig'ishi uchun bo'lingan (boshidagi izoh asl faylda besh band):

tsx
// dropdown-menu.tsx — shadcn/ui'dan KO'CHIRILGAN (20/#03) va
// moslangan: alohida paket, 4 qism, bizning tokenlar,
// modal={false} (CSP), loop, band balandligi 44 px
import type { ComponentProps } from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "@/ui/cn.ts";
import { portalJoyi } from "@/ui/portal-joyi.ts";

export function DropdownMenu({
  modal = false,
  ...props
}: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
  return (
    <DropdownMenuPrimitive.Root
      data-slot="dropdown-menu"
      modal={modal}
      {...props}
    />
  );
}

export function DropdownMenuTrigger(
  props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>,
) {
  return (
    <DropdownMenuPrimitive.Trigger
      data-slot="dropdown-menu-trigger"
      {...props}
    />
  );
}

// Portal: ro'yxat <main> oxirida (portal-joyi.ts) — ota-elementning
// overflow va z-index'i uni kesmaydi (18/#10 dagi toast kabi).
// Joylashuvni (pastda/tepada, ekran chetida) Floating UI
// hisoblaydi — style prop orqali (CSSOM, CSP'ga mos)
export function DropdownMenuContent({
  className,
  align = "start",
  sideOffset = 4,
  loop = true,
  ...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
  return (
    <DropdownMenuPrimitive.Portal container={portalJoyi()}>
      <DropdownMenuPrimitive.Content
        data-slot="dropdown-menu-content"
        sideOffset={sideOffset}
        align={align}
        loop={loop}
        className={cn(
          "z-50 flex min-w-(--radix-dropdown-menu-trigger-width) flex-col rounded-burchak border border-chegara bg-yuza p-1 text-matn shadow-soya",
          className,
        )}
        {...props}
      />
    </DropdownMenuPrimitive.Portal>
  );
}

// Band — div role="menuitem" (Radix): fokus strelkalar bilan
// ko'chadi, sichqoncha ustiga kelganda ham fokus shu bandga
// o'tadi — shuning uchun faqat focus: holati bezaladi
export function DropdownMenuItem({
  className,
  ...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item>) {
  return (
    <DropdownMenuPrimitive.Item
      data-slot="dropdown-menu-item"
      className={cn(
        "flex min-h-bosish cursor-default items-center gap-2 rounded-burchak px-3 py-2 whitespace-nowrap outline-none select-none focus:bg-asosiy focus:text-asosiy-ustida data-disabled:opacity-50",
        className,
      )}
      {...props}
    />
  );
}

Sinflar qatori 70 belgidan uzun — kanon kodi o'zgartirilmaydi. shadcn'dan farqlar:

  • ComponentProps<typeof DropdownMenuPrimitive.Root> — primitivning props turi to'liq (React + TS III). Radix'ga yangi props qo'shilsa, o'ram ham qabul qiladi.
  • Tokenlar: bg-popover, bg-accent o'rnida bizning bg-yuza, bg-asosiy (oldingi darsdagi @theme). dark: kerak emas — tokenlar light-dark() bilan almashadi.
  • min-w-(--radix-dropdown-menu-trigger-width) — Radix'ning CSS o'zgaruvchisi (tugma eni): ro'yxat tugmadan tor bo'lmaydi.
  • min-h-bosish — band 44 px. shadcn'da h-7 (28 px) — barmoqqa kichik (WCAG 2.5.5).
  • loop = true — 18-qismdagi Menyu miz kabi oxiridan boshiga aylanadi.
  • modal = false va container={portalJoyi()} — o'lchov bilan: modal menyu xavfsizlik siyosatini buzadi (1 buzilish), <body> dagi portal axe'da "landmark'dan tashqarida" chiqadi. Ikkalasi — Radix chuqur darsida jonli misol bilan.

portal-joyi.ts — bitta funksiya (kanon):

ts
// portal-joyi.ts — Radix portallari qayerga chiziladi (20/#03).
// Sukut bo'yicha <body> oxiriga — <main> dan tashqarida. axe
// ("region") buni "landmark'dan tashqaridagi kontent" deydi.
// <main> ichida ham joylashuv buzilmaydi: Floating UI
// position: fixed bilan ekran koordinatasida joylaydi
export function portalJoyi(): HTMLElement | null {
  return document.querySelector("main");
}

5.3 Ishlatilishi: statistika hisoboti

BatafsilStatistika.tsx da o'zgarish — nomlar almashdi, tuzilish o'sha (kanon):

diff
-import { Menyu } from "../../ui/Menyu.tsx";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "../../ui/dropdown-menu.tsx";
...
-      <Menyu>
-        <Menyu.Tugma className="hisobot-tugma">
+      {/* 20/#03: shadcn/ui DropdownMenu (Radix) — 18/#26 dagi o'z
+          Menyu'miz o'rniga. Klaviatura, fokus qaytishi, aria-* —
+          Radix'da; onSelect — band tanlanganda, menyu o'zi
+          yopiladi */}
+      <DropdownMenu>
+        <DropdownMenuTrigger className="hisobot-tugma">
           Hisobotni yuklab olish
-        </Menyu.Tugma>
-        <Menyu.Royxat>
-          <Menyu.Band
-            onTanla={() =>
+        </DropdownMenuTrigger>
+        <DropdownMenuContent>
+          <DropdownMenuItem
+            onSelect={() =>
               faylniYuklabBer(
                 statistikaCsv(s),
                 "statistika.csv",

Menyu.Tugma → DropdownMenuTrigger, Menyu.Royxat → DropdownMenuContent, Menyu.Band + onTanla → DropdownMenuItem + onSelect. Compound naqshni bilganimiz uchun ko'chish qatorma-qator bo'ldi. Foydalanuvchi menyusida yana bitta detal: ism sahifaning o'ng chetida, shuning uchun ro'yxat ham o'ngga tekislanadi:

diff
-        <Menyu.Royxat>
-          <Menyu.Band onTanla={chiqish}>Chiqish</Menyu.Band>
-        </Menyu.Royxat>
-      </Menyu>
+        </DropdownMenuTrigger>
+        {/* Ism o'ng chetda — ro'yxat ham o'ng chetga tekislanadi */}
+        <DropdownMenuContent align="end">
+          <DropdownMenuItem onSelect={chiqish}>
+            Chiqish
+          </DropdownMenuItem>
+        </DropdownMenuContent>
+      </DropdownMenu>

asos.css dan menyuning 20 qator CSS'i o'chdi — stil endi dropdown-menu.tsx dagi Tailwind sinflarida.

5.4 Testlar: Radix fokusni kechiktirib qo'yadi

Menyu.test.tsx dagi oltita testdan to'rttasi qoldi. «controlled» va «qism tashqarida — aniq xato» testlari o'chirildi: bu endi Radix'ning ishi, uni Radix o'z testlarida tekshiradi. Biz faqat bizga kerak xulqni tekshiramiz: modal emas, aylanadi, fokus qaytadi.

Ikki yangilik bor. Birinchisi — menyu click bilan emas, Enter bilan ochiladi (fireEvent.keyDown(tugma(), { key: "Enter" })). Ikkinchisi — fokusni kutish (kanon):

ts
// Radix fokusni keyingi vazifada (setTimeout) qo'yadi — ochilganda
// ham, strelkalarda ham. Shuning uchun har tugmadan keyin kutamiz
const fokusKut = (matn: string) =>
  waitFor(() => expect(fokusda()).toBe(matn));

Bizning useMenyu fokusni darhol ko'chirardi. Radix esa uni setTimeout bilan, keyingi vazifada qo'yadi (Event loop darsidagi makrovazifa). Test ochilishi bilan fokusni o'qisa, u hali eski joyda. waitFor — shart bajarilguncha qayta-qayta tekshiradi. Xuddi shu sabab bilan kanonning 18-qism brauzer ssenariysiga keyinroq har tugmadan keyin 60 ms kutish qo'shildi: uch urinishdan birida fokus erta o'qilgan edi. Testlar chuqur — React Testing Library darsida.

bash
git add -A
git commit

Commit (kanon): feat: menyular — shadcn DropdownMenu (Radix, modal emas), portal <main> da.

5.5 Natija va narx

Tekshiruvlar (haqiqiy, holat 03):

  • npm test — node:test tests 198 · pass 198, Vitest Tests 65 passed (65) (67 edi — ikki test Radix'ga o'tdi).
  • lint, format:check, tip, knip — exit 0. axe — 6 ko'rinishda 0 buzilish.
  • Brauzer: 159, 72 va 100 tekshiruv — hammasi o'tdi. 20-qismning yangi tekshiruvlaridan menyuga oidlari:
text
✅ menyu: ↓ — ochildi, fokus birinchi bandda (menuitem): ["CSV (Excel uchun)","menuitem"]
✅ menyu: aria-expanded va data-state (tugma, ro'yxat): ["true","open","open"]
✅ menyu: portal — <main> ichida, statistika panelidan tashqarida: [true,true]
✅ menyu: modal emas — fon aria-hidden emas, body'da style yo'q: [0,null]
✅ menyu: nomi — tugma matni (aria-labelledby): "Hisobotni yuklab olish"
✅ menyu: End — oxirgi band: "JSON"
✅ menyu: Esc — yopildi, fokus tugmada, data-state closed: [true,true,"closed"]
✅ menyu: tashqariga bosish — yopildi: false
✅ menyu: CSP buzilishi yo'q: []
✅ konsolda xato yo'q: []

Endi narx. Birinchi yuklashdagi JavaScript (gzip):

Birinchi yuklash JS (gzip), vazifalar
  • holat 02 — o'z Menyu151,9 KB
  • holat 03 — Radix menyu+27.2 KB179,1 KB

Manba: O'lchandi: holat 02 va 03, gzip -9, natijalar/tekshiruv-02.txt va tekshiruv-03.txt, 2026-10-14

326 qator o'z kodimiz o'rniga +27.2 KB. Eng kattalari (03-paketlar.txt): @radix-ui/react-menu 4.6 KB, Floating UI (joylashuvni hisoblovchi kutubxona) 4.5 + 4.4 KB, @radix-ui/react-collection 3.4 KB. Qolgani — fokus, tashqariga bosish va portal uchun mayda paketlar.

Bu ko'pmi? Qaror: Radix menyu qoladi — evaziga ichma-ich menyu, ekran chetida joylashuvni o'zgartirish, sinalgan ARIA va keyingi komponentlar bilan bir xil "til". Lekin har kilobayt hisobda: keyingi og'ir kutubxonalar (buyruqlar oynasi, sudrab tartiblash) faqat kerak bo'lganda yuklanadi — lazy va code splitting qoidasi.

6. Ko'p uchraydigan xatolar

6.1 Tooltip Provider'siz

Tooltip'ni boshqa komponentlardek "o'zi" qo'ysangiz, ilova umuman chizilmaydi:

jsx
import * as Tooltip from "@radix-ui/react-tooltip";

export default function App() {
  return (
    <Tooltip.Root>
      <Tooltip.Trigger>🗑</Tooltip.Trigger>
      <Tooltip.Content>Vazifani o'chirish</Tooltip.Content>
    </Tooltip.Root>
  );
}

Konsolda:

text
Uncaught Error: `Tooltip` must be used within `TooltipProvider`

Tarjimasi: "Tooltip TooltipProvider ichida ishlatilishi kerak". Bu Compound komponentlar darsidagi "qism ota tashqarisida — aniq xato" himoyasining o'zi, faqat Radix'niki. Davosi — ilova ildizida bitta <Tooltip.Provider> (shadcn'da <TooltipProvider>). Har tooltipga alohida o'rash shart emas.

6.2 Testda click menyuni ochmaydi

Sardor Vitest'da yozdi: fireEvent.click(tugma), keyin getByRole("menu") — "topilmadi". Brauzerda esa menyu ochilyapti! Sabab: Radix DropdownMenu tugmasi pointerdown ni tinglaydi. fireEvent.click esa faqat click yuboradi, haqiqiy pointerdown → pointerup → click zanjirini emas. pointerdown da ochish qulay: odam bosib, bandga surib, qo'yib yuborsa — bitta harakatda tanlov.

Davosi — testda klaviatura: fireEvent.keyDown(tugma, { key: "Enter" }), kanondagidek. Ustiga test Bobur (faqat klaviatura) yo'lini ham tekshiradi.

6.3 Boshqalari

Belgi Sabab Davosi
ro'yxat ota-element ichida kesilib qoladi Portal siz Content Content ni Portal ichiga
belgilash bandi bosilganda menyu yopiladi tanlovdan keyingi standart — yopish onSelect={(e) => e.preventDefault()}
onClick ishlaydi, lekin Enter bilan — har xil amal onClick da, mantiq ikki joyda amal — onSelect da
Cannot find module 'radix-ui' (TS2307) CLI importni yozdi, paketni o'rnatmadi shadcn I: npm i -E bilan o'rnating
tsc -b — TS2320 (react-select) umumiy radix-ui paketining turlari alohida @radix-ui/react-* paketlar
DataTable kodi ishlamaydi (useReactTable) qo'llanma v8, loyihada v9 Jadvallar I

7. Mashqlar

Mashqlar kurs/mashqlar/20/03-dropdown-menu/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi komponent?

Vaziyatga mos shadcn komponenti nomini yozing (DropdownMenu, Dialog, AlertDialog, Sheet, Popover, Tooltip, Command):

  • Jadval qatoridagi "…" ostida "Ko'rish, Nusxalash, O'chirish" →
  • Ikonka tugma ustiga kelganda "Yuklab olish" yozuvi →
  • O'ng chetdan chiqadigan buyurtma tahrirlash formasi →
  • Ctrl+K bosilganda hamma sahifa va taomlar bo'yicha qidiruv →
  • "Hisobni butunlay o'chirasizmi?" — orqa fonni bosish yopmasin →
  • Sana va narx oralig'i tanlanadigan kichik filtr paneli →
Yechim
  • DropdownMenu — amallar ro'yxati: bittasi tanlanadi, menyu yopiladi, strelkalar ishlaydi.
  • Tooltip — faqat matnli izoh, ustiga kelish yoki fokusda chiqadi.
  • Sheet — Dialog xulqi, lekin ekran chetidan va uzunroq ish uchun.
  • Command — qidiruv maydoni + filtrlangan ro'yxat (cmdk).
  • AlertDialog — xavfli amal, aniq javob talab qiladi.
  • Popover — ichida oddiy boshqaruv elementlari, Tab bilan yuriladi. Menyu bo'lsa, surgichni strelkalar bilan siljitib bo'lmasdi.

2-mashq (o'rta): qo'lda yozilgan ro'yxatni DropdownMenu'ga

Kodda «Ulashish» menyusi qo'lda yozilgan: useState bilan ochiladi, ichida oddiy tugmalar. Klaviatura, Esc va fokus qaytishi yo'q. Uni Radix DropdownMenu'ga o'tkazing:

  1. «Ulashish» — Trigger, ro'yxat — Portal ichidagi Content.
  2. «Telegram» va «Havolani nusxalash» — Item, «Instagram» — disabled (hali ulanmagan).
  3. Oxirgi band oldidan Separator, oxirgi band — «Shikoyat qilish».
  4. Tanlangan amal pastda chiqsin: Oxirgi amal: ….

Ishora: «Beshta qism» bo'limidagi misol. disabled band — xuddi <button disabled> kabi prop. Darsdagidek modal={false} qo'ying.

jsx
import { useState } from "react";

export default function App() {
  const [open, setOpen] = useState(false);
  const [last, setLast] = useState("—");
  function choose(name) {
    setLast(name);
    setOpen(false);
  }
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Ulashish</button>
      {open && (
        <div>
          <button onClick={() => choose("Telegram")}>Telegram</button>
          <button onClick={() => choose("Havola")}>
            Havolani nusxalash
          </button>
          <button disabled>Instagram</button>
          <hr />
          <button onClick={() => choose("Shikoyat")}>
            Shikoyat qilish
          </button>
        </div>
      )}
      <p>Oxirgi amal: {last}</p>
    </div>
  );
}

Ekranda:

text
Ulashish
Oxirgi amal: Telegram
Yechim
jsx
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

export default function App() {
  const [last, setLast] = useState("—");
  return (
    <div>
      <DropdownMenu.Root modal={false}>
        <DropdownMenu.Trigger>Ulashish</DropdownMenu.Trigger>
        <DropdownMenu.Portal>
          <DropdownMenu.Content align="start" loop>
            <DropdownMenu.Item onSelect={() => setLast("Telegram")}>
              Telegram
            </DropdownMenu.Item>
            <DropdownMenu.Item onSelect={() => setLast("Havola")}>
              Havolani nusxalash
            </DropdownMenu.Item>
            <DropdownMenu.Item disabled>Instagram</DropdownMenu.Item>
            <DropdownMenu.Separator />
            <DropdownMenu.Item onSelect={() => setLast("Shikoyat")}>
              Shikoyat qilish
            </DropdownMenu.Item>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>
      <p>Oxirgi amal: {last}</p>
    </div>
  );
}

Ekranda:

text
Ulashish
Oxirgi amal: Telegram

Ekran bir xil, farq — ichida. open state va choose kerak emas: ochish-yopishni Radix boshqaradi. Strelkalar (Instagram o'tkazib yuboriladi), Esc, tashqariga bosish va fokus qaytishi qo'shildi. Malika uchun — role="menu", menuitem va «Instagram» da aria-disabled.

3-mashq (qiyin): «Ko'rinish» menyusi haqiqiy ro'yxat bilan

«Bahor» taomlari ro'yxatiga «Ko'rinish» menyusini qo'shing:

  1. Taomlar: Osh 35 000, Lag'mon 28 000, Manti 30 000, Ko'k choy 5 000. Ro'yxat <ul> da, har qator: Osh — 35000.
  2. CheckboxItem «Faqat 30 000 gacha» — belgilansa, narxi 30 000 dan oshiqlar yashiriladi. Bosilganda menyu yopilmasin.
  3. RadioGroup — tartib: «Nomi» (alifbo) yoki «Narxi» (arzonidan qimmatiga). Boshida — «Nomi».
  4. ItemIndicator bilan va ● belgilar.

Ishora: «Bandlar turlari» bo'limidagi misol. Saralash uchun nusxa oling: [...items].sort(...) (sort va taqqoslash funksiyasi darsida). Satrlarni solishtirish — a.name.localeCompare(b.name).

Yechim
jsx
import { useState } from "react";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

const DISHES = [
  { name: "Osh", price: 35000 },
  { name: "Lag'mon", price: 28000 },
  { name: "Manti", price: 30000 },
  { name: "Ko'k choy", price: 5000 },
];

export default function App() {
  const [cheap, setCheap] = useState(false);
  const [sortBy, setSortBy] = useState("nomi");
  const visible = DISHES.filter((d) => !cheap || d.price <= 30000);
  const sorted = [...visible].sort((a, b) =>
    sortBy === "nomi"
      ? a.name.localeCompare(b.name)
      : a.price - b.price,
  );
  return (
    <div>
      <DropdownMenu.Root modal={false}>
        <DropdownMenu.Trigger>Ko'rinish</DropdownMenu.Trigger>
        <DropdownMenu.Portal>
          <DropdownMenu.Content align="start">
            <DropdownMenu.CheckboxItem
              checked={cheap}
              onCheckedChange={setCheap}
              onSelect={(e) => e.preventDefault()}
            >
              <DropdownMenu.ItemIndicator>
                ✓{" "}
              </DropdownMenu.ItemIndicator>
              Faqat 30 000 gacha
            </DropdownMenu.CheckboxItem>
            <DropdownMenu.Separator />
            <DropdownMenu.RadioGroup
              value={sortBy}
              onValueChange={setSortBy}
            >
              <DropdownMenu.RadioItem value="nomi">
                <DropdownMenu.ItemIndicator>
                  ●{" "}
                </DropdownMenu.ItemIndicator>
                Nomi
              </DropdownMenu.RadioItem>
              <DropdownMenu.RadioItem value="narxi">
                <DropdownMenu.ItemIndicator>
                  ●{" "}
                </DropdownMenu.ItemIndicator>
                Narxi
              </DropdownMenu.RadioItem>
            </DropdownMenu.RadioGroup>
          </DropdownMenu.Content>
        </DropdownMenu.Portal>
      </DropdownMenu.Root>
      <ul>
        {sorted.map((d) => (
          <li key={d.name}>
            {d.name} — {d.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

Ekranda:

text
Ko'rinish
Ko'k choy — 5000
Lag'mon — 28000
Manti — 30000

«Faqat 30 000 gacha» Osh'ni yashirdi, menyu ochiq qoldi. «Narxi» ro'yxatni arzonidan tizdi va menyuni yopdi. Ro'yxat holatdan hisoblanadi (visible, sorted) — alohida "saralangan ro'yxat" state'i yo'q (State strukturasini loyihalash qoidasi).

4-mashq: Vazifalar qadami

feat/dropdown-menu branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni qiling: paketni -E bilan o'rnating, ui/dropdown-menu.tsx va ui/portal-joyi.ts ni yozing, ikki menyuni ko'chiring, eski uch faylni va CSS'ni o'chiring, Menyu.test.tsx ni yangilang. Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm test — node 198, Vitest 65. Menyu testi "topilmadi" bilan yiqilsa — click o'rnida keyDown Enter va fokusKut.
  • npm run tip — 0 xato. TS2320 chiqsa — umumiy radix-ui o'rnida alohida paket.
  • «Hisobotni yuklab olish» → ↓ → CSV. Esc — fokus tugmada.
  • DevTools Elements: menyu ochiq turganda ro'yxat <main> ichida, <body> da style atributi yo'q.
  • npm run build — birinchi yuklash ~179 KB gzip (oldin ~152).

8. Real ishda

  • Admin panellar va SaaS — ko'p mahsulot interfeysi shu "g'ishtlar"dan: yon panel, qator menyusi, yon oyna, buyruqlar oynasi. Bloklar bilan skelet bir kunda tayyor.
  • Vakansiyada "shadcn/ui" — komponent faylini o'zgartira oladigan odam kutiladi, faqat add qiladigan emas. Har o'zgarish kanondagidek fayl boshida izoh bilan yozilsin — kod ko'rigida birinchi savol "nega?".
  • Intervyu savollari: "shadcn/ui'ning kutubxonadan farqi nima?", "DropdownMenu va Popover qachon qaysi biri?", "onSelect va onClick farqi?", "nega testda menyu click bilan ochilmaydi?".

Xulosa

  • Dashboard — tayyor g'ishtlar: Sidebar, DropdownMenu, Dialog, Sheet, Popover, Tooltip, Command, Field, DataTable, Chart. Bloklar — ulardan yig'ilgan tayyor sahifa.
  • DropdownMenu = Root → Trigger → Portal → Content → Item. Amal — onSelect da, menyu o'zi yopiladi; e.preventDefault() — ochiq qoldiradi.
  • CheckboxItem, RadioGroup/RadioItem, ItemIndicator, Label, Separator, Sub — menyuning boshqa bandlari. Holat sizning state'ingizda.
  • shadcn fayli — primitiv ustidagi ingichka o'ram: data-slot, standart props, sinflar. Fayl sizniki: kanon uni tokenlar, 44 px, modal={false} va <main> dagi portal bilan moslagan.
  • Kutubxona bepul emas: Radix menyu +27.2 KB. Evaziga sinalgan klaviatura va ARIA, va keyingi komponentlar uchun bir xil til.

Keyingi dars: Radix Primitives chuqur — asChild bilan o'z Tugma mizni trigger qilish, data-state bilan stil, controlled menyu, fokus yo'li, portal va nega modal={false} (CSP o'lchovi bilan); Radix, Base UI va React Aria taqqoslovi.

Manbalar

  • shadcn/ui: Dropdown Menu, Dialog, Sheet, Popover, Sidebar, Chart, Blocks — ui.shadcn.com/docs (2026-10, CLI 4.21.4)
  • shadcn/ui: Forms — React Hook Form va Field — ui.shadcn.com/docs/forms/react-hook-form
  • Radix Primitives: Dropdown Menu — radix-ui.com/primitives/docs/components/dropdown-menu (2.1.25)
  • WAI-ARIA APG: Menu Button Pattern — w3.org/WAI/ARIA/apg/patterns/menu-button
  • Kanon: vazifalar holat 03, natijalar/tekshiruv-03.txt, 03-paketlar.txt, 03-radix-ui-umumiy-paket.txt (2026-10-14)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
shadcn/ui komponentlari real ilovada: DropdownMenu, Dialog, Sheet, Popover, Form va DataTable — IlmHamroh