IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya10/30-dars22 daqiqa
Mundarija (33)

React'da modal, drawer, popover va tooltip: qatlamli UI va Floating UI

Qisqacha: Sahifa ustiga chiqadigan to'rt xil qatlam bor. Modal (dialog) — sahifani to'xtatib, javob kutadi. Drawer — ekran chetidan chiqadigan modal (telefonda pastdan). Popover — tugma yonida ochiladigan, sahifani to'xtatmaydigan panel. Tooltip — fokus yoki sichqonchada chiqadigan qisqa izoh, faqat qo'shimcha ma'lumot. Joylashuvni Floating UI hisoblaydi: offset (oraliq), flip (joy bo'lmasa ag'darish), shift (ekrandan chiqmaslik). vazifalar da modal — native <dialog>, tooltip — Radix.

Bu darsda

  • To'rt qatlam turini xulqi bo'yicha farqlaysiz va har vaziyatga to'g'risini tanlaysiz.
  • Native <dialog> va Radix Dialog/AlertDialog'ni taqqoslaysiz, skroll bloklashni CSP'ga mos yo'l bilan qilasiz.
  • vaul drawer'ni va uning narxini bilasiz.
  • Radix Popover va Tooltip'ning a11y "simlarini" jonli ko'rasiz: aria-expanded, role="tooltip", aria-describedby.
  • Floating UI'ning offset, flip, shift g'oyasini va Radix'dagi mos props'larni bilasiz.
  • Modalni URL bilan bog'laysiz — «Orqaga» uni yopadi.
  • vazifalar ga Radix tooltip qo'shishni boshlaysiz (holat 11).

Oldin bilishingiz kerak: <dialog> va Popover API JS'dan, Modal oyna va skroll bloklash, Tooltip, dropdown va toast, Radix Primitives chuqur, Toast bildirishnomalar.

1. Nega bu kerak?

«Bahor» panelida to'rtta yangi talab paydo bo'ldi:

  1. Taomni o'chirishdan oldin so'rash — "Osh menyudan olinsinmi?"
  2. Telefonda filtrlar pastdan chiqadigan panelda bo'lsin.
  3. "Sana" tugmasi yonida kichik kalendar ochilsin.
  4. Faqat ikonkali tugmalar ustida qisqa izoh chiqsin.

To'rttasi ham "sahifa ustida suzib yuradigan narsa". Sardor hammasini bitta Modal komponenti bilan qilmoqchi edi. Lekin ular xulqi bilan farq qiladi: kimdir sahifani to'xtatadi, kimdir yo'q; kimdir fokusni ichiga oladi, kimdir oldirmaydi. Noto'g'ri tur tanlansa, Bobur (faqat klaviatura) qamalib qoladi yoki Malika (ekran o'quvchi) muhim savolni eshitmaydi.

Hayotiy o'xshatish: oshxonadagi to'rt xil "to'xtash". Kassir "to'lovni tasdiqlaysizmi?" deydi — siz javob bermaguncha navbat to'xtaydi (modal). Menyu stoli chetidan chiqadi (drawer). Ofitsiant yoningizda bir lahza turib, ziravor tanlatadi — boshqa ishni to'xtatmaydi (popover). Taom yonidagi kichik yozuv "achchiq" (tooltip) — o'qisangiz ham, o'qimasangiz ham bo'ladi.

2. Xarita: to'rt qatlam

Sahifani to'xtatadimi Fokus Qanday yopiladi
Modal (dialog) ha — fon "o'lik" ichkariga, tuzoq Esc, tugma; fon bosish — ixtiyoriy
Drawer ha (modal turi) ichkariga, tuzoq Esc, surish, fon
Popover yo'q ichkariga, tuzoqsiz Esc, tashqariga bosish
Tooltip yo'q ko'chmaydi Esc, fokus/sichqoncha ketganda

Va ularning rollari: modal — role="dialog" (tasdiq oynasi — alertdialog), popover — Radix'da dialog (modal emas), tooltip — role="tooltip". Menyu (role="menu") — alohida tur, shadcn/ui II va Radix chuqur darslarida o'tildi.

Tanlash yo'li:

flowchart TD
  A{"Javob kerakmi /<br/>ish to'xtashi kerakmi?"} -->|ha| B
  B{"Telefonda<br/>chetdan chiqsinmi?"}
  B -->|ha| D["Drawer"]
  B -->|yo'q| M["Modal (dialog)"]
  A -->|yo'q| C{"Ichida bosiladigan<br/>narsa bormi?"}
  C -->|ha| P["Popover"]
  C -->|yo'q, faqat matn| T["Tooltip"]

Nimaga qarang: tooltip va popover orasidagi chegara — "ichida tugma yoki maydon bormi?". Tooltip ichida bosiladigan narsa bo'lmaydi: u fokus olmaydi, Bobur unga yetib bora olmaydi.

3. Modal: native <dialog> va Radix Dialog

3.1 Native <dialog> — vazifalar dagi yo'l

<dialog> va Popover API darsida brauzerning o'z modalini yozganmiz: showModal() fonni o'chiradi (inert), fokusni ichkariga oladi, Esc yopadi, yopilganda fokus qaytadi. vazifalar dagi o'chirish tasdig'i 13-qismdan beri shunday.

React'da u ref orqali boshqariladi:

jsx
import { useRef, useState } from "react";

export default function App() {
  const dialog = useRef(null);
  const [result, setResult] = useState("—");
  return (
    <div>
      <button onClick={() => dialog.current.showModal()}>
        Osh'ni o'chirish
      </button>
      <p>Javob: {result}</p>
      <dialog
        ref={dialog}
        aria-labelledby="sarlavha"
        onClose={(e) => setResult(e.currentTarget.returnValue)}
      >
        <h2 id="sarlavha">Osh menyudan olinsinmi?</h2>
        <form method="dialog">
          <button value="yoq">Bekor qilish</button>{" "}
          <button value="ha">O'chirish</button>
        </form>
      </dialog>
    </div>
  );
}

Bu blok tekshiruvchida ishga tushirilmaydi: u ishlatadigan jsdom'da showModal yo'q (TypeError: ref.current.showModal is not a function). Saytdagi natija oynasi esa haqiqiy brauzer — «Ishga tushir» bilan sinang. «O'chirish» ni bosing: «Javob: ha». Esc bossangiz — «Javob: » bo'sh qoladi (returnValue o'rnatilmadi).

  • form method="dialog" — forma yuborilganda oyna yopiladi, bosilgan tugmaning value si returnValue ga yoziladi. JavaScript'siz.
  • onClose — har yopilishda (tugma, Esc) bitta hodisa.
  • aria-labelledby — oyna nomi sarlavhadan.

3.2 Radix Dialog: nimasi bor, nimasi qo'shimcha

Radix Dialog'ning anatomiyasini UI kutubxonalar xaritasi va shadcn/ui II darslarida ko'rdik: Root, Trigger, Portal, Overlay, Content, Title, Description, Close. Native bilan taqqoslash:

native <dialog> Radix Dialog
Fon "o'lik" showModal() — top layer, inert aria-hidden qo'shnilarga + pointer-events: none
Fokus tuzog'i brauzerda Radix kodi
Skroll bloklash yo'q bor (react-remove-scroll, <style> tegi)
Animatsiya (chiqish) @starting-style va allow-discrete data-state + forceMount
CSP style-src 'self' 0 buzilish 1 buzilish (kanon kutubxonalar.txt)

Natija oynasida Radix Dialog bor — skroll bloklash <style> ini UI kutubxonalar xaritasi darsida o'lchagan edik (yangi <style>: 1). Kanon tajribasida alohida Radix Dialog — faqat React'ga nisbatan +13.6 KB va 1 CSP buzilishi.

Radix'da oyna nomi va izohi — Title va Description. Izoh kerak bo'lmasa, Radix hujjati aria-describedby={undefined} ni Content ga berishni aytadi — aks holda dev rejimda ogohlantirish chiqadi. (Natija oynasidagi Radix Dialog 1.2.0 bu ogohlantirishni konsolga chiqarmaydi — qoidani hujjatdan bilamiz.)

3.3 AlertDialog

AlertDialog — xavfli amalni tasdiqlash uchun alohida primitiv (@radix-ui/react-alert-dialog, natija oynasida yo'q). Dialog'dan uch farqi:

  • role="alertdialog" — ekran o'quvchi uni qat'iyroq e'lon qiladi;
  • fonni bosish uni yopmaydi — aniq javob kerak;
  • tugmalar — Cancel va Action; ochilganda fokus Cancel da (xavfsiz tanlov).
tsx
<AlertDialog.Root>
  <AlertDialog.Trigger>Menyuni tozalash</AlertDialog.Trigger>
  <AlertDialog.Portal>
    <AlertDialog.Overlay />
    <AlertDialog.Content>
      <AlertDialog.Title>
        Butun menyu o'chirilsinmi?
      </AlertDialog.Title>
      <AlertDialog.Description>
        12 taom va ularning narxlari o'chadi.
      </AlertDialog.Description>
      <AlertDialog.Cancel>Bekor qilish</AlertDialog.Cancel>
      <AlertDialog.Action onClick={tozala}>
        Tozalash
      </AlertDialog.Action>
    </AlertDialog.Content>
  </AlertDialog.Portal>
</AlertDialog.Root>

Esc esa Radix AlertDialog'ni yopadi (kanon tajribasida shadcn varianti: escdanKeyin: oyna false). HeroUI'da standart bo'yicha — yo'q (boshqa kitlar).

3.4 Skroll bloklash — CSP'ga mos yo'l

Native <dialog> ochiq turganda orqadagi sahifa aylanaveradi. Modal oyna va skroll bloklash darsida buni sof CSS bilan hal qilgan edik:

css
html:has(dialog:modal) {
  overflow: hidden;
  scrollbar-gutter: stable;
}
dialog {
  overscroll-behavior: contain;
}

Bu — oddiy CSS faylda, JavaScript <style> yasamaydi. Ya'ni style-src 'self' bilan to'liq mos. Radix ham, React Aria ham skroll'ni JavaScript bilan bloklaydi: Radix <style> tegi bilan (CSP buzadi), React Aria esa kompyuter brauzerlarida <html> ning style xususiyatini o'zgartiradi (CSSOM — CSP ruxsat beradi). Bir muammo — uch yechim, CSP bilan munosabati har xil.

4. Drawer: vaul

Drawer (tortma) — ekran chetidan chiqadigan modal. Telefonda pastdan chiqib, barmoq bilan pastga surib yopiladi. shadcn'da Drawer — vaul kutubxonasi ustida.

tsx
import { Drawer } from "vaul";

<Drawer.Root snapPoints={[0.4, 1]}>
  <Drawer.Trigger>Filtrlar</Drawer.Trigger>
  <Drawer.Portal>
    <Drawer.Overlay />
    <Drawer.Content>
      <Drawer.Title>Filtrlar</Drawer.Title>
      {/* filtr formasi */}
    </Drawer.Content>
  </Drawer.Portal>
</Drawer.Root>

snapPoints — panel to'xtaydigan balandliklar: 40% va to'liq ekran. Anatomiya Radix Dialog'niki — chunki vaul uning ustiga qurilgan: vaul 1.1.2 ning yagona bog'liqligi — @radix-ui/react-dialog. Ustiga surish (drag) xulqi va animatsiya qo'shilgan.

Narxi (kanon kutubxonalar.txt, faqat React'ga nisbatan): +21.4 KB va 3 ta CSP buzilishi — Radix Dialog'ning <style> i va vaul'ning o'z CSS'i (manba kodida document.createElement('style') bor). Yana bir fakt: npm'da vaul'ning oxirgi relizi — 1.1.2, 2024-12-14. Kutubxona tanlashda "kim va qachon yangilaydi?" savolini eslang (UI kutubxonalar xaritasi dagi 7-savol).

CSP'li ilovada muqobil: native <dialog> + Modal oyna va skroll bloklash darsidagi CSS "sheet" — pastda joylashgan, @starting-style bilan pastdan chiqadi. Surib yopish bo'lmaydi, lekin Esc, fon va tugma ishlaydi.

5. Popover

5.1 Radix Popover: simlarni o'qiymiz

Popover — tugmaga bog'langan, sahifani to'xtatmaydigan panel: sana tanlash, filtr, rang tanlash. Ichida tugma va maydonlar bo'ladi — Tab bilan yuriladi. shadcn/ui II darsida ochib ko'rgan edik. Endi atributlarini o'qiymiz:

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

function inspect(open) {
  setTimeout(() => {
    const btn = document.querySelector("button");
    const panel = document.querySelector("[data-side]");
    console.log("ochiq:", open);
    const attr = (name) => btn.getAttribute(name);
    console.log("tugma aria-haspopup:", attr("aria-haspopup"));
    console.log("tugma aria-expanded:", attr("aria-expanded"));
    console.log("panel role:", panel.getAttribute("role"));
    console.log("data-side:", panel.dataset.side);
    console.log("data-align:", panel.dataset.align);
  }, 100);
}

export default function App() {
  return (
    <Popover.Root onOpenChange={inspect}>
      <Popover.Trigger>Sana</Popover.Trigger>
      <Popover.Portal>
        <Popover.Content sideOffset={8}>
          <label>
            Kun: <input type="date" />
          </label>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}

Konsolda:

text
ochiq: true
tugma aria-haspopup: dialog
tugma aria-expanded: true
panel role: dialog
data-side: bottom
data-align: center
  • aria-haspopup="dialog" — tugma "dialog ochadi" deydi. Menyuda menu edi.
  • role="dialog" — panel ham dialog, lekin modal emas: fon tirik, aria-hidden qo'yilmaydi. Shuning uchun popover ichida sarlavha bo'lmasa ham ishlaydi, lekin aria-label bersangiz, Malika "Sana, dialog" ni eshitadi.
  • data-side="bottom", data-align="center" — standart joylashuv: tugma ostida, markazda. sideOffset={8} — tugmadan 8 piksel pastroqda.

Popover'da modal prop ham bor (standart false). modal={true} — fon "o'ladi", fokus tuzoqqa tushadi va skroll bloklanadi — Radix chuqur dagi menyu kabi <style> bilan. Kanon tajribasida Popover (modal siz) — +24.0 KB, CSP 0.

5.2 Native Popover API

Brauzerda ham popover bor: popover atributi va popovertarget (W-53, C-116 darslarida). Top layer'da ochiladi, Esc va tashqariga bosish bilan yopiladi — JavaScript'siz. Ikkita narsa yetishmaydi: tugmaga joylashish (CSS anchor positioning bilan qilinadi, lekin barcha brauzerlarda bir xil emas) va ochilganda fokusni ichkariga olib o'tish. Radix Popover ikkalasini ham beradi — Floating UI orqali.

6. Tooltip

6.1 Uch qoida

Tooltip, dropdown va toast darsida WCAG 1.4.13 ning uch talabini ko'rgan edik — sichqoncha yoki fokusda chiqadigan kontent:

  1. Yopiladigan (dismissible) — Esc bilan, sichqonchani qimirlatmasdan.
  2. Ustiga o'tsa bo'ladigan (hoverable) — sichqoncha tooltip ustiga o'tsa, u yo'qolmaydi.
  3. Turadigan (persistent) — odam o'zi ketmaguncha yo'qolmaydi.

React'da yana bitta qoida qo'shiladi: tooltip — faqat qo'shimcha. Telefonda hover yo'q, sensorli ekranda tooltip umuman chiqmaydi. Shuning uchun muhim ma'lumot tugmaning o'zida (matn yoki aria-label), tooltip'da emas. Kanon ui/tooltip.tsx izohi aynan shuni aytadi: "Tooltip — faqat QO'SHIMCHA ma'lumot (klaviatura yorlig'i)… telefonda (bosish) — yo'q. Shuning uchun muhim matn tugmaning o'zida yoziladi".

6.2 Radix Tooltip: Bobur yo'li

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

function log(label) {
  setTimeout(() => {
    const btn = document.querySelector("button");
    const id = btn.getAttribute("aria-describedby");
    const tip = id && document.getElementById(id);
    console.log(label, "| data-state:", btn.dataset.state);
    console.log(
      "  aria-describedby →",
      tip ? `${tip.getAttribute("role")}: ${tip.textContent}` : null,
    );
    console.log("  fokus:", document.activeElement.textContent);
  }, 50);
}

export default function App() {
  return (
    <Tooltip.Provider delayDuration={300}>
      <Tooltip.Root
        onOpenChange={(open) => log(open ? "ochildi" : "yopildi")}
      >
        <Tooltip.Trigger>Buyruqlar</Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content sideOffset={4}>
            Tezkor ochish: Ctrl+K
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
}

Konsolda:

text
ochildi | data-state: instant-open
  aria-describedby → tooltip: Tezkor ochish: Ctrl+K
  fokus: Buyruqlar
yopildi | data-state: closed
  aria-describedby → null
  fokus: Buyruqlar

Natija oynasini bosing va klaviaturada Tab, keyin Esc ni bosing:

Qadam Fokus Tooltip
Tab «Buyruqlar» tugmasi darhol ochildi (instant-open)
Esc «Buyruqlar» — joyida yopildi

Uch narsani o'qiymiz:

  1. instant-open — klaviatura fokusida tooltip kechikishsiz ochildi. delayDuration={300} faqat sichqoncha uchun: hover'da holat delayed-open bo'ladi. Tasodifiy sichqoncha harakati tooltip'larni "chaqnatmasin", Bobur esa kutib o'tirmasin.
  2. aria-describedby → tooltip — tugma ochiq tooltip matniga bog'landi, u role="tooltip". Malika "Buyruqlar, tugma, Tezkor ochish: Ctrl+K" ni eshitadi: nom — tugmadan, izoh — tooltip'dan.
  3. Esc — fokus joyida. Tooltip yopildi, fokus tugmada qoldi. Fokus hech qachon tooltip ichiga kirmaydi — u bosilmaydi.

Tooltip.Provider — barcha tooltiplarning umumiy sozlamasi: delayDuration va skipDelayDuration (bir tooltip ochiq bo'lsa, qo'shnisi kechikishsiz ochiladi — asboblar paneli bo'ylab yurganda qulay). Provider'siz Tooltip — xato (shadcn/ui II dagi «Tooltip Provider'siz»).

Tekshirib ko'ring: Sardor tooltip ichiga «Batafsil» havolasini qo'ydi. Nima noto'g'ri?

Javob

Tooltip fokus olmaydi: Bobur Tab bossa, fokus tugmadan keyingi elementga o'tadi va tooltip yopiladi — havolaga yetib bo'lmaydi. Sichqoncha bilan ham qiyin: tooltip harakatga sezgir, ko'p hollarda ustiga o'tish paytida yopiladi. Telefonda esa umuman ko'rinmaydi. Ichida bosiladigan narsa bo'lsa — bu popover («Xarita: to'rt qatlam» bo'limidagi daraxt).

7. Floating UI: offset, flip, shift

7.1 Joylashuv nega qiyin

Popover yoki tooltip tugma yonida turishi kerak. Lekin tugma ekranning pastki chetida bo'lsa-chi? Pastga ochilgan panel ekrandan chiqib ketadi. O'ng chetda bo'lsa — kesiladi. Sahifa aylantirilsa — panel tugmaga ergashishi kerak.

Bu hisob-kitobni Floating UI kutubxonasi qiladi (Anchor positioning va C-116 darslarida tilga olingan). Radix, shadcn, Mantine va boshqalar ichida aynan shu. Ishlash tartibi — middleware (oraliq bosqichlar) zanjiri:

flowchart LR
  A["Boshlang'ich joy<br/>placement: bottom"] --> B["offset<br/>8 px nari"]
  B --> C["flip<br/>joy yo'qmi? → top"]
  C --> D["shift<br/>chetdan chiqmasin"]
  D --> E["size<br/>bo'yini cheklash"]
  E --> F["x, y → style"]
  • offset — tugmadan oraliq.
  • flip — tanlangan tomonda joy yo'q bo'lsa, qarama-qarshi tomonga ag'daradi (pastdan tepaga).
  • shift — panel ekran chetidan chiqib ketsa, uni o'q bo'ylab suradi.
  • size — mavjud balandlikka qarab panelni kichraytirish (uzun ro'yxatlar uchun).
  • autoUpdate — skroll, o'lcham o'zgarishida joyni qayta hisoblash.

Nimaga qarang: tartib muhim — avval ag'darish, keyin surish. Oxirida natija — ikki raqam (x, y), ular style xususiyatiga yoziladi. Bu CSSOM, <style> tegi emas — shuning uchun Floating UI CSP bilan chiqishadi (Radix chuqur).

7.2 Floating UI to'g'ridan va Radix orqali

Kutubxonaning o'zi (@floating-ui/react 0.27.20, natija oynasida yo'q):

tsx
import {
  useFloating, offset, flip, shift, autoUpdate,
} from "@floating-ui/react";

const { refs, floatingStyles, placement } = useFloating({
  placement: "bottom",
  middleware: [offset(8), flip(), shift({ padding: 8 })],
  whileElementsMounted: autoUpdate,
});

<button ref={refs.setReference}>Sana</button>
<div ref={refs.setFloating} style={floatingStyles}>…</div>

placement — haqiqiy joy (flip dan keyin "top" bo'lishi mumkin). Radix'da xuddi shu narsalar props bo'lib keladi:

Floating UI Radix Content prop'i DOM'da
placement side, align data-side, data-align
offset(8) sideOffset={8} —
flip() + shift() avoidCollisions (standart true) data-side o'zgaradi
shift({ padding }) collisionPadding —
size() — --radix-popover-content-available-height

Shuning uchun Radix chuqur darsida "animatsiya yo'nalishini data-side ga qarab tanlang" degan edik: side="bottom" yozsangiz ham, flip uni top qilishi mumkin.

Tekshirib ko'ring: Tooltip tugma ostida chiqishi kerak (side="bottom"), lekin tugma ekranning eng pastida. Ekranda nima ko'rasiz va data-side nima bo'ladi?

Javob

Tooltip tugma ustida chiqadi va data-side="top" bo'ladi. avoidCollisions (ya'ni flip) pastda joy yo'qligini ko'rib, qarama-qarshi tomonga ag'dardi. Agar strelka yoki animatsiya side prop'iga qarab yozilgan bo'lsa, u noto'g'ri tomonga qaraydi — shuning uchun stil [data-side=top] ga qarab yoziladi. Natija oynasida buni ko'rib bo'lmaydi (tekshiruvchida joylashuv hisoblanmaydi), haqiqiy brauzerda — oynani kichraytirib sinang.

8. URL bilan modal

Jasur aka buyurtmani tahrirlash oynasini ochib, havolani Sardorga yubormoqchi bo'ldi. Lekin URL o'zgarmagan — Sardor oddiy ro'yxatni ko'rdi. Yana bir muammo: telefonda «Orqaga» bosilganda oyna yopilmadi, butun sahifa orqaga ketdi.

Yechim — oynaning holatini URL'da saqlash (So'rov parametrlari darsidagi g'oya): ?tahrir=12 bo'lsa — oyna ochiq.

jsx
import * as Dialog from "@radix-ui/react-dialog";
import {
  MemoryRouter, useLocation, useNavigate, useSearchParams,
} from "react-router";

function Orders() {
  const [params, setParams] = useSearchParams();
  const navigate = useNavigate();
  const location = useLocation();
  const id = params.get("tahrir");
  console.log("URL:", location.pathname + location.search);
  return (
    <div>
      <button onClick={() => setParams({ tahrir: "12" })}>
        Tahrirlash: Osh
      </button>
      <Dialog.Root
        open={id !== null}
        onOpenChange={(open) => {
          if (!open) navigate(-1); // ochilish yozuviga qaytamiz
        }}
      >
        <Dialog.Portal>
          <Dialog.Content aria-describedby={undefined}>
            <Dialog.Title>Buyurtma №{id}</Dialog.Title>
            <Dialog.Close>Yopish</Dialog.Close>
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>
    </div>
  );
}

export default function App() {
  return (
    <MemoryRouter initialEntries={["/buyurtmalar"]}>
      <Orders />
    </MemoryRouter>
  );
}

Konsolda:

text
URL: /buyurtmalar
URL: /buyurtmalar?tahrir=12
URL: /buyurtmalar
  • Ochish — setParams URL'ga ?tahrir=12 ni yangi tarix yozuvi sifatida qo'shadi (push). Oyna — open={id !== null}, ya'ni controlled, holat manbai URL.
  • Yopish (Esc, «Yopish», fon) — navigate(-1): tarixda bir qadam orqaga. URL yana /buyurtmalar.
  • «Orqaga» tugmasi brauzerda ham xuddi shunday ishlaydi: URL o'zgaradi → id null → oyna yopiladi. Telefondagi odatiy harakat endi to'g'ri.
  • Havola /buyurtmalar?tahrir=12 — ochilgan oyna bilan keladi.

Natija oynasida manzil satri yo'q, shuning uchun MemoryRouter (SPA routing darsidagi qoida); loyihada — createBrowserRouter. Chuqurroq naqsh — fon sahifani saqlab qoladigan "marshrut-modal" (React Router'da location.state bilan) — katta ilovalarda ishlatiladi.

Diqqat: navigate(-1) bitta holatda xato: odam oynaga havola orqali to'g'ridan kirgan bo'lsa (yangi tab). Unda tarixda orqada hech narsa yo'q — navigate(-1) ilovadan chiqib ketadi yoki hech narsa qilmaydi. Buni 3-mashqda tuzatasiz.

9. Vazifalar qadami: Radix tooltip (holat 11 boshlanadi)

9.1 Nima va nega

Keyingi darsda vazifalar ga Ctrl+K bilan ochiladigan buyruqlar oynasi qo'shiladi. Uning tugmasi ustida tooltip yorliqni ko'rsatadi: «Tezkor ochish: Ctrl+K». Bugun shu tooltip'ning o'zini qo'shamiz. Commit — keyingi darsda, holat 11 bitta qadam.

Modal, popover va drawer uchun kanonda o'zgarish yo'q: tasdiq oynasi native <dialog> da qoladi (CSP — UI kutubxonalar xaritasi va Radix chuqur darslaridagi o'lchov), popover va vaul — faqat tajriba. Kanon raqamlari (kutubxonalar.txt, faqat React'ga nisbatan):

Qatlamli UI kutubxonalari: JS (gzip) — faqat React'ga qo'shimcha
  • Radix DialogCSP 113,6 KB
  • Radix TooltipCSP 018,5 KB
  • vaul (drawer)CSP 321,4 KB
  • Radix PopoverCSP 024 KB

Manba: O'lchandi: tajriba/kitlar, Vite 8.3.3, React 19.3.0, Chrome 155, natijalar/tajriba/kutubxonalar.txt, 2026-10-14

Bu raqamlar "faqat React" ilovaga nisbatan. vazifalar da Radix menyu allaqachon bor — umumiy qismlar (Floating UI, Portal, Presence) qayta yuklanmaydi. Shuning uchun tooltip vazifalar ning birinchi yuklashiga atigi +4.1 KB qo'shdi (kanon tuzatish o'lchovi). Tooltip lazy qilinmadi: u birinchi ekranda kerak.

bash
git switch -c feat/buyruqlar
npm i -E @radix-ui/react-tooltip@1.3.0

9.2 ui/tooltip.tsx

shadcn'dan ko'chirilgan (npx shadcn@4.21.4 add tooltip, shadcn/ui I dagi oqim) va moslangan. Kanon fayli (sarlavha izohi qisqartirilgan):

tsx
// tooltip.tsx — shadcn/ui'dan KO'CHIRILGAN (20/#10) va moslangan:
// alohida paket @radix-ui/react-tooltip, bizning tokenlar,
// portal — <main> da, o'q (Arrow) olib tashlandi
import type { ComponentProps } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/ui/cn.ts";
import { portalJoyi } from "@/ui/portal-joyi.ts";

// Provider — kechikish hamma tooltip uchun bitta (Qobiq'da): bir
// tooltip ochiq bo'lsa, qo'shnisi kechikishsiz ochiladi
// (skipDelayDuration)
export function TooltipProvider({
  delayDuration = 300,
  ...props
}: ComponentProps<typeof TooltipPrimitive.Provider>) {
  return (
    <TooltipPrimitive.Provider
      delayDuration={delayDuration}
      {...props}
    />
  );
}

export function Tooltip(
  props: ComponentProps<typeof TooltipPrimitive.Root>,
) {
  return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
}

export function TooltipTrigger(
  props: ComponentProps<typeof TooltipPrimitive.Trigger>,
) {
  return (
    <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
  );
}

// Matn trigger'ga aria-describedby bilan bog'lanadi (Radix): ekran
// o'quvchi tugma nomidan keyin tooltip matnini ham o'qiydi
export function TooltipContent({
  className,
  sideOffset = 4,
  ...props
}: ComponentProps<typeof TooltipPrimitive.Content>) {
  return (
    <TooltipPrimitive.Portal container={portalJoyi()}>
      <TooltipPrimitive.Content
        data-slot="tooltip-content"
        sideOffset={sideOffset}
        className={cn(
          "z-50 rounded-burchak bg-matn px-3 py-1.5 text-sm text-fon shadow-soya",
          className,
        )}
        {...props}
      />
    </TooltipPrimitive.Portal>
  );
}

Bugungi dars g'oyalari kodda:

  • portalJoyi() — tooltip <main> ichiga chiziladi, <body> ga emas: axe'ning region qoidasi (Radix chuqur dagi menyu bilan bir xil sabab).
  • Arrow olib tashlandi — flip paytida strelka yo'nalishini data-side ga qarab almashtirish kerak bo'lardi; kichik tooltip'ga bu murakkablik shart emas.
  • bg-matn text-fon — teskari ranglar: matn rangi fonga, fon matnga. Qorong'i temada o'zi almashadi (shadcn/ui I dagi tokenlar).
  • TooltipContent dagi Tailwind qatori 76 belgi — kanon kodi o'zgarmaydi.

9.3 Provider — Qobiq.tsx da

diff
+import { TooltipProvider } from "../ui/tooltip.tsx";
 ...
-      <FoydalanuvchiMenyusi />
+      {/* 20/#10–#11: buyruqlar oynasi (Ctrl+K) va foydalanuvchi
+          menyusi bir qatorda. TooltipProvider — tooltip'lar uchun
+          umumiy kechikish */}
+      <TooltipProvider>
+        <div className="flex flex-wrap items-center justify-between gap-2">
+          {kirgan && <BuyruqlarOynasi />}
+          <FoydalanuvchiMenyusi />
+        </div>
+      </TooltipProvider>

BuyruqlarOynasi — keyingi darsning komponenti. Uning tugmasida tooltip shunday ulanadi (kanon, qisqartirilgan):

tsx
<Tooltip>
  <TooltipTrigger asChild>
    <Tugma id="buyruqlar-tugma" aria-haspopup="dialog" onClick={och}>
      <Search size={16} />
      Buyruqlar
    </Tugma>
  </TooltipTrigger>
  <TooltipContent>{`Tezkor ochish: ${YORLIQ}`}</TooltipContent>
</Tooltip>

asChild — trigger bizning Tugma (Radix chuqur). Tugma matni «Buyruqlar» — nom tugmada, tooltip — faqat yorliq haqida qo'shimcha.

9.4 Tekshiruv (holat 11, kanon e2e-20)

Tooltip uchun ikki tekshiruv (natija-20-11.txt, Chrome 155):

text
✅ tooltip: klaviatura fokusida chiqdi, tugmaga bog'langan (aria-describedby): ["Tezkor ochish: Ctrl+K",true,"Control+K"]
✅ tooltip: Esc — yopildi, fokus joyida: [true,"buyruqlar-tugma"]

Bu «Radix Tooltip: Bobur yo'li» bo'limidagi natijaning haqiqiy Chrome'dagi takrori: fokusda chiqdi, aria-describedby bilan bog'landi, Esc — fokus joyida. Massivdagi "Control+K" — tugmaning aria-keyshortcuts atributi (keyingi darsda). axe — 7 ko'rinishda 0 buzilish (11-axe.txt). Testlar, build va commit — keyingi darsda, Ctrl+K bilan birga.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
modal ochiq, orqa sahifa aylanadi native <dialog> skroll'ni bloklamaydi html:has(dialog:modal) { overflow: hidden }
style-src xatosi modal ochilganda Radix Dialog/vaul <style> native <dialog> + CSS
tooltip ichidagi havolaga yetib bo'lmaydi tooltip fokus olmaydi popover
telefonda muhim ma'lumot ko'rinmaydi u faqat tooltip'da matnni tugmaga
strelka noto'g'ri tomonga qaraydi stil side prop'ga qarab [data-side=…]
«Orqaga» oynani yopmaydi oyna holati faqat useState da holat URL'da
havola bilan kirganda «Yopish» ilovadan chiqaradi navigate(-1) — tarix bo'sh 3-mashqdagi tekshiruv
Radix Dialog'da izoh ogohlantirishi Description yo'q aria-describedby={undefined}

11. Mashqlar

Mashqlar kurs/mashqlar/20/10-qatlamli-ui/ da.

1-mashq (oson): qaysi qatlam?

Har vaziyatga bittasi: modal, drawer, popover yoki tooltip.

  • «Kartani o'chirish» tasdig'i:
  • Ikonka tugma ustida «Nusxalash»:
  • «Narx oralig'i» tugmasi yonida ikki maydon va «Qo'llash»:
  • Telefonda pastdan chiqadigan savat:
Yechim
  • Tasdiq — javob kerak, ish to'xtaydi: modal (aniqrog'i alertdialog).
  • «Nusxalash» — faqat matn, qo'shimcha: tooltip (tugmada aria-label ham bo'lsin).
  • Ichida maydonlar va tugma bor, sahifa to'xtamaydi: popover.
  • Chetdan chiqadigan modal, telefonda: drawer.

2-mashq (o'rta): «Qo'llash» popover'ni yopsin

«Narx» popover'i ichida ikki tugma: «35 000 gacha» va «Qo'llash». «35 000 gacha» filtrni tanlaydi, popover ochiq qoladi. «Qo'llash» — filtrni sahifaga yozadi va popover'ni yopadi. Pastda Filtr: 35 000 gacha.

Ishora: popover controlled — open va onOpenChange (Radix chuqur dagi uchlik).

text
Narx
Filtr: 35 000 gacha
Yechim
jsx
import { useState } from "react";
import * as Popover from "@radix-ui/react-popover";

export default function App() {
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState("—");
  const [filter, setFilter] = useState("—");
  function apply() {
    setFilter(draft);
    setOpen(false);
  }
  return (
    <div>
      <Popover.Root open={open} onOpenChange={setOpen}>
        <Popover.Trigger>Narx</Popover.Trigger>
        <Popover.Portal>
          <Popover.Content sideOffset={8} aria-label="Narx filtri">
            <button onClick={() => setDraft("35 000 gacha")}>
              35 000 gacha
            </button>{" "}
            <button onClick={apply}>Qo'llash</button>
          </Popover.Content>
        </Popover.Portal>
      </Popover.Root>
      <p>Filtr: {filter}</p>
    </div>
  );
}

Ekranda:

text
Narx
Filtr: 35 000 gacha

Popover yopildi — «Ekranda» da uning tugmalari yo'q. Ikki holat bor: draft (popover ichidagi tanlov) va filter (qo'llangani). «Qo'llash» bosilmasa, sahifa o'zgarmaydi — formadagi "saqlash" mantiqi. setOpen(false) — popover controlled bo'lgani uchun kod bilan yopish mumkin; fokus Radix tomonidan «Narx» tugmasiga qaytariladi. aria-label — popover dialog'ining nomi.

3-mashq (qiyin): havola bilan kirilgan modal

«URL bilan modal» bo'limidagi kodni tuzating: ilovaga to'g'ridan /buyurtmalar?tahrir=12 havolasi bilan kirilsa, yopish navigate(-1) emas, ?tahrir siz manzilga almashtirish (replace) bo'lsin. Ilova ichidan ochilganda esa — avvalgidek navigate(-1).

Ishora: ilova ichida birinchi tarix yozuvining location.key qiymati — "default" (React Router darslaridagi chekka holat). setParams({}, { replace: true }) — joriy yozuvni almashtiradi.

Havola bilan kirib, Esc bosilgandan keyingi konsol:

text
URL: /buyurtmalar?tahrir=12
URL: /buyurtmalar
Yechim
jsx
import * as Dialog from "@radix-ui/react-dialog";
import {
  MemoryRouter, useLocation, useNavigate, useSearchParams,
} from "react-router";

function Orders() {
  const [params, setParams] = useSearchParams();
  const navigate = useNavigate();
  const location = useLocation();
  const id = params.get("tahrir");
  console.log("URL:", location.pathname + location.search);

  function close() {
    if (location.key === "default") {
      // havola bilan kirilgan: orqada ilova yozuvi yo'q
      setParams({}, { replace: true });
    } else {
      navigate(-1); // ilova ichida ochilgan
    }
  }

  return (
    <div>
      <button onClick={() => setParams({ tahrir: "12" })}>
        Tahrirlash: Osh
      </button>
      <Dialog.Root
        open={id !== null}
        onOpenChange={(open) => !open && close()}
      >
        <Dialog.Portal>
          <Dialog.Content aria-describedby={undefined}>
            <Dialog.Title>Buyurtma №{id}</Dialog.Title>
            <Dialog.Close>Yopish</Dialog.Close>
          </Dialog.Content>
        </Dialog.Portal>
      </Dialog.Root>
    </div>
  );
}

export default function App() {
  return (
    <MemoryRouter initialEntries={["/buyurtmalar?tahrir=12"]}>
      <Orders />
    </MemoryRouter>
  );
}

Konsolda:

text
URL: /buyurtmalar?tahrir=12
URL: /buyurtmalar

Oyna ochiq holda boshlandi (havola), Esc — location.key hali "default", shuning uchun replace: tarixda bitta yozuv qoladi, «Orqaga» endi ilovadan chiqadi — kutilgan xulq. Ilova ichida «Tahrirlash» bosilgan bo'lsa, yangi yozuvning kaliti tasodifiy satr bo'ladi va navigate(-1) ishlaydi. Radix fokusni Trigger ga qaytaradi — bu misolda trigger yo'q (oyna URL'dan ochilgan), shuning uchun haqiqiy ilovada fokusni o'zingiz tegishli joyga qo'yasiz (Fokus boshqaruvi).

4-mashq: Vazifalar qadami

feat/buyruqlar branch'ida: @radix-ui/react-tooltip@1.3.0 ni aniq versiya bilan o'rnating, src/ui/tooltip.tsx ni yarating («ui/tooltip.tsx» bo'limi) va Qobiq.tsx da TooltipProvider ni qo'shing. Commit hali qilinmaydi — keyingi darsda BuyruqlarOynasi bilan birga.

Yechim

Tekshiruv ro'yxati:

  • package.json da "@radix-ui/react-tooltip": "1.3.0" — ^ siz.
  • npm run tip — 0 xato (hali hech kim Tooltip ni ishlatmaydi — knip ishlatilmagan eksport haqida ogohlantirishi mumkin; keyingi darsda yo'qoladi).
  • TooltipContent — portalJoyi() bilan <main> ga.
  • Qobiq'da TooltipProvider — foydalanuvchi menyusi atrofida; BuyruqlarOynasi importi keyingi darsda qo'shiladi.

12. Real ishda

  • Dizayn tizimlari — Dialog, Sheet (drawer), Popover, Tooltip — har birida alohida komponent va qoida: "tooltip ichida tugma yo'q", "tasdiq — AlertDialog". Kod ko'rigida turini noto'g'ri tanlash — ko'p uchraydigan izoh.
  • Mobil veb — «Orqaga» bilan yopiladigan oynalar (URL'da holat) foydalanuvchi uchun tabiiy; ko'p ilovalar buni unutadi.
  • Intervyu savollari: "modal va popover farqi?", "fokus tuzog'i (focus trap) nima va qachon kerak?", "tooltip a11y talablari?", "Floating UI'da flip va shift farqi?", "modalni URL bilan qanday bog'laysiz?".

Xulosa

  • To'rt qatlam: modal (to'xtatadi, javob kutadi), drawer (chetdan modal), popover (to'xtatmaydi, ichida boshqaruv), tooltip (faqat matn, fokus ko'chmaydi).
  • Native <dialog> — fokus, Esc, inert bepul; skroll bloklash — CSS (html:has(dialog:modal)), CSP'ga mos. Radix Dialog va vaul — <style> bilan.
  • AlertDialog: alertdialog roli, fon bosilmaydi, fokus «Bekor qilish» da.
  • Tooltip: klaviaturada instant-open, aria-describedby → role="tooltip", Esc'da fokus joyida; muhim matn — tugmada.
  • Floating UI: offset → flip → shift → size; Radix'da sideOffset, avoidCollisions, data-side.
  • URL bilan modal: ochish — push, yopish — navigate(-1) (havola bilan kirilganda — replace).

Keyingi dars: Command palette va klaviatura yorliqlari — Ctrl+K bilan vazifalar buyruqlar oynasi: cmdk, o'zbekcha apostrofni biladigan qidiruv, Mac'da K, maydon ichidagi yorliqlar va cmdk'ni faqat kerak bo'lganda yuklash.

Manbalar

  • MDN: <dialog>, HTMLDialogElement.showModal(), Popover API — developer.mozilla.org
  • Radix Primitives: Dialog, Alert Dialog, Popover, Tooltip — radix-ui.com/primitives (Tooltip 1.3.0)
  • Floating UI: Middleware (offset, flip, shift, size), autoUpdate — floating-ui.com (@floating-ui/react 0.27.20)
  • vaul — github.com/emilkowalski/vaul (1.1.2)
  • WCAG 2.2: 1.4.13 Content on Hover or Focus — w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus
  • WAI-ARIA APG: Dialog (Modal), Alert Dialog, Tooltip patterns — w3.org/WAI/ARIA/apg/patterns
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React'da modal, drawer, popover va tooltip: qatlamli UI va Floating UI — IlmHamroh