IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik9/32-dars21 daqiqa
Mundarija (36)

JavaScript <dialog> va Popover API: modal oyna va popover'ni JS'dan boshqarish

Qisqacha: dialog.showModal() — modal oyna ochadi, dialog.close("qiymat") — yopadi va returnValue ga javobni yozadi; close hodisasi — oyna har qanday yo'l bilan yopilganda, cancel — Esc bosilganda keladi. Tasdiqlash oynasini Promise qaytaradigan funksiyaga o'rash qulay: await askConfirm("O'chirilsinmi?"). Popover uchun — showPopover(), hidePopover(), togglePopover(), holat o'zgarishi esa beforetoggle va toggle hodisalarida. Modal ochiq turganda sahifaning qolgan qismi inert bo'ladi.

Bu darsda

  • showModal, show, close, requestClose va returnValue bilan oynani boshqarasiz.
  • close va cancel hodisalari farqini bilib, "saqlanmagan o'zgarish" holatini ushlaysiz.
  • Promise qaytaradigan tasdiqlash oynasini yozasiz.
  • showPopover, hidePopover, togglePopover, beforetoggle va toggle bilan popover'ni JavaScript'dan boshqarasiz.
  • vazifalar da o'chirishni <dialog> bilan tasdiqlaysiz.

Oldin bilishingiz kerak: dialog: native modal oyna, Popover API, Invoker commands, Dinamik UI'da qulaylik (a11y), Promise asoslari.

1. Nega bu kerak?

HTML qismida <dialog> va popover'ni JavaScript'siz ishlatdik: command="show-modal" tugmasi oynani ochadi, form method="dialog" yopadi, popovertarget menyuni ochib-yopadi. Ko'p holatda shu yetarli.

Lekin uch narsa uchun JavaScript kerak:

  • Javobni olish. "Bron bekor qilinsinmi?" — «Ha» yoki «Yo'q». Javobga qarab kod davom etadi: serverga so'rov yuborish yoki hech narsa qilmaslik. Bu — eski confirm() ning o'rni. Brauzer muhiti darsida aytgan edik: confirm butun sahifani muzlatadi va uni bezab bo'lmaydi.
  • Vaqt. "Bron qabul qilindi" xabari 3 soniyadan keyin o'zi yo'qolsin. Tugma bunday qila olmaydi — taymer kerak.
  • Hodisalar. Oyna yopilganda fokusni to'g'ri joyga qaytarish, Esc ni "saqlanmagan o'zgarish bor" deb to'xtatish, menyu ochilganda ma'lumot yuklash.

Bugun <dialog> va popover'ning JavaScript tomonini ko'ramiz. Yangi element o'rganmaymiz — HTML'dagi elementlarga "boshqaruv pulti" qo'shamiz.

2. <dialog> metodlari

2.1 Ochish va yopish

js
const dialog = document.getElementById("cancel-dialog");

dialog.showModal(); // modal: fon inert, Esc yopadi, top layer
dialog.show(); // non-modal: fon bilan ishlash mumkin
dialog.close(); // yopish
dialog.close("ha"); // yopish va returnValue = "ha"
console.log(dialog.open); // true yoki false

Ikki so'zni eslaylik. Modal oyna — javob bermaguningizcha orqadagi sahifa ishlamaydi; non-modal — oyna ochiq, lekin orqadagi sahifa bilan ham ishlasa bo'ladi. Top layer — hamma narsadan, hatto eng katta z-index dan ham ustun turadigan alohida qavat.

dialog darsidagi hamma narsa shu metodlarning ishi: showModal() — oynani top layer'ga chiqaradi, orqasiga ::backdrop qo'yadi, sahifaning qolgan qismini "uxlatadi" va fokusni oyna ichiga o'tkazadi. command="show-modal" tugmasi ham aynan showModal() ni chaqiradi.

open — o'qish uchun. Uni true qilib oynani "ochish" mumkin, lekin bu show() kabi non-modal ochadi: fon ishlayveradi, Esc yopmaydi. Modal kerak bo'lsa — faqat showModal().

2.2 returnValue: oyna nima deb javob berdi

Oyna yopilganda uning returnValue xususiyatida javob qoladi. U uch yo'l bilan yoziladi:

  • form method="dialog" ichidagi tugma bosilsa — tugmaning value i.
  • dialog.close("qiymat") — berilgan qiymat.
  • Esc bilan yopilsa — o'zgarmaydi.

Oxirgisi — tuzoq. returnValue oldingi ochilishdan qolib ketadi. Chrome 154 da sinadik: oynada «Ha» bosildi ("ha"), keyin oyna yana ochilib Esc bilan yopildi — returnValue hamon "ha". Kod buni "Ha" deb tushunadi! Shuning uchun har ochishdan oldin tozalang:

js
dialog.returnValue = ""; // eski javob qolmasin
dialog.showModal();

2.3 Qachon xato beradi

Chrome 154 da uch xatoni oldik:

  • Non-modal ochiq oynaga showModal(): InvalidStateError: ... The dialog is already open as a non-modal dialog, and therefore cannot be opened as a modal dialog. — "oyna allaqachon non-modal ochiq, uni modal qilib bo'lmaydi". Avval close().
  • Sahifaga qo'shilmagan oyna: InvalidStateError: ... The element is not in a Document. — createElement("dialog") dan keyin avval append qiling.
  • Modal ochiq oynaga yana showModal() — xato yo'q, hech narsa o'zgarmaydi.

Tekshirib ko'ring: Sardor modal oynani dialog.open = true bilan ochdi. Oyna ko'rindi, lekin Esc ishlamaydi va orqadagi tugmalar bosiladi. Nega?

Javob

open atributi oynani non-modal ochadi — xuddi show() kabi. Top layer, ::backdrop, inert fon va Esc — faqat showModal() da. To'g'risi: dialog.showModal().

3. Hodisalar: close va cancel

3.1 Oyna hayoti

stateDiagram-v2
  [*] --> Yopiq
  Yopiq --> Ochiq: showModal()
  Ochiq --> cancel: Esc / requestClose()
  cancel --> Ochiq: preventDefault()
  cancel --> close
  Ochiq --> close: close(qiymat) / form tugmasi
  close --> Yopiq
  • close — oyna har qanday yo'l bilan yopilganda keladi: tugma, close(), Esc. Javobni shu yerda o'qiysiz.
  • cancel — faqat yopish so'rovi paytida keladi: Esc, Android'dagi "orqaga" tugmasi yoki requestClose(). U close dan oldin keladi va bekor qilinadigan (cancelable): preventDefault() oynani ochiq qoldiradi.

Chrome 154 da Esc bosganimizda tartib aynan shunday edi: cancel, keyin close. Tugma bilan yopilganda esa faqat close.

3.2 requestClose: "foydalanuvchi kabi" yopish

close() oynani shartsiz yopadi. requestClose(qiymat) esa xuddi Esc kabi: avval cancel hodisasini yuboradi, uni hech kim to'xtatmasa — yopadi va returnValue ga qiymatni yozadi. Sinadik: requestClose("rc") → cancel, keyin close, returnValue — "rc". Bu metod 2025-yil mayidan barcha asosiy brauzerlarda bor (Baseline, web-features). Tugmada ham bor: command="request-close".

3.3 «Bahor»: tasdiqlash oynasi Promise bilan

Eng foydali naqsh: oynani Promise qaytaradigan funksiyaga o'rash. Natijada kod confirm() kabi o'qiladi, lekin sahifa muzlamaydi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
  dialog { border: 1px solid #c9d6cf; border-radius: 0.75rem; }
  dialog::backdrop { background: rgb(0 0 0 / 0.5); }
  .danger { color: #b3261e; border-color: #b3261e; }
</style>
<p>Bron: 6-stol, bugun 19:00</p>
<button id="cancel-booking" type="button">Bronni bekor qilish</button>
<p id="result" role="status"></p>

<dialog id="confirm" aria-labelledby="confirm-title">
  <form method="dialog">
    <h2 id="confirm-title">Bron bekor qilinsinmi?</h2>
    <p id="confirm-text"></p>
    <button value="no" autofocus>Yo'q, qolsin</button>
    <button value="yes" class="danger">Ha, bekor qilish</button>
  </form>
</dialog>

<script>
  const dialog = document.getElementById("confirm");

  function askConfirm(text) {
    document.getElementById("confirm-text").textContent = text;
    dialog.returnValue = "";
    dialog.showModal();
    return new Promise((resolve) => {
      dialog.addEventListener(
        "close",
        () => resolve(dialog.returnValue === "yes"),
        { once: true },
      );
    });
  }

  const result = document.getElementById("result");
  const button = document.getElementById("cancel-booking");
  button.addEventListener("click", async () => {
    const yes = await askConfirm("6-stol, 19:00 — bron o'chadi.");
    console.log(`Javob: ${yes}`);
    result.textContent = yes ? "Bron bekor qilindi" : "Bron saqlandi";
  });
</script>

«Bronni bekor qilish» ni bosing. Oyna ochiladi, fokus «Yo'q, qolsin» da (autofocus). Uchta yo'lni sinang:

  • «Ha, bekor qilish» → konsolda Javob: true.
  • «Yo'q, qolsin» → Javob: false.
  • Esc → Javob: false: returnValue bo'sh qoldi, "yes" emas.

Uchalasini sinadik — natija aynan shunday, va har safar oyna yopilgach fokus «Bronni bekor qilish» tugmasiga qaytdi.

Qatorma-qator:

  • askConfirm(text) — Promise qaytaradi. await gacha kod "kutadi", lekin sahifa ishlayveradi: animatsiyalar, taymerlar, tarmoq.
  • dialog.returnValue = "" — oldingi javob qolib ketmasin ("returnValue" bo'limidagi tuzoq).
  • { once: true } — Listener opsiyalari darsidan: tinglovchi bir marta ishlab, o'zini olib tashlaydi. Usiz har ochilishda yangi tinglovchi qo'shilib, eskilari ham ishlayverardi.
  • resolve(dialog.returnValue === "yes") — faqat aniq «Ha» true beradi. Esc, «Yo'q», kutilmagan qiymat — hammasi false. Xavfli amalda xavfsiz standart.
  • autofocus «Yo'q» da — xuddi shu sabab: Bobur Enter ni tasodifan bossa, bron o'chmaydi.

Tekshirib ko'ring: { once: true } ni olib tashlasak va oynani uch marta ochib yopsak, uchinchi yopilishda resolve necha marta chaqiriladi?

Javob

Uch marta — har ochilishda qo'shilgan uchala tinglovchi ham ishlaydi. Oldingi ikki Promise allaqachon bajarilgan, ularga qayta resolve hech narsa qilmaydi. Lekin tinglovchilar to'planib boraveradi (xotira va ortiqcha ish). Murakkabroq kodda bu "bir bosishda ikki marta o'chirish" kabi xatoga olib keladi. once: true — har ochilishga bitta tinglovchi.

3.4 cancel ni to'xtatish: saqlanmagan o'zgarish

Bron formasiga izoh yozilyapti. Mehmon tasodifan Esc bosdi — yozgani yo'qolmasin:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  textarea { width: 100%; font: inherit; }
  #warn { color: #b3261e; min-height: 1.5em; }
  button { font: inherit; }
</style>
<button id="open-note" type="button">Izoh yozish</button>
<dialog id="note" aria-labelledby="note-title">
  <h2 id="note-title">Bron uchun izoh</h2>
  <textarea id="note-text" rows="3"></textarea>
  <p id="warn" role="alert"></p>
  <button id="save" type="button">Saqlash</button>
  <button id="discard" type="button">Yozganimni o'chirish</button>
</dialog>
<script>
  const note = document.getElementById("note");
  const text = document.getElementById("note-text");
  const warn = document.getElementById("warn");

  const openNote = document.getElementById("open-note");
  openNote.addEventListener("click", () => {
    warn.textContent = "";
    note.showModal();
  });

  note.addEventListener("cancel", (event) => {
    if (text.value.trim() !== "") {
      event.preventDefault(); // Esc oynani yopmaydi
      warn.textContent = "Izoh saqlanmagan. Saqlang yoki o'chiring.";
      console.log("cancel to'xtatildi");
    }
  });

  document.getElementById("save").addEventListener("click", () => {
    note.close("saqlandi");
  });
  document.getElementById("discard").addEventListener("click", () => {
    text.value = "";
    note.close("ochirildi");
  });
  note.addEventListener("close", () => console.log(note.returnValue));
</script>

«Izoh yozish» → maydonga Deraza yonida deb yozing → Esc. Oyna yopilmaydi, qizil ogohlantirish chiqadi va konsolda cancel to'xtatildi. «Saqlash» — saqlandi. Bo'sh maydonda Esc — oyna oddiygina yopiladi. Biz yozib, Esc va «Saqlash» ni bosib tekshirdik.

Ikki qoida:

  • Faqat haqiqiy sabab bilan. cancel ni doim to'xtatish — foydalanuvchini qamab qo'yish. Oynadan chiqishning aniq yo'li (bu yerda ikki tugma) doim ko'rinib tursin.
  • Ogohlantirish role="alert" bilan — Malika'ning ekran o'quvchisi uni darhol o'qiydi.

3.5 Tashqariga bosib yopish

dialog darsida closedby="any" ni ko'rgan edik — ::backdrop ga bosilsa oyna yopiladi (light dismiss). Chrome 154 da sinadik: tashqariga bosish ham cancel ni, keyin close ni yuboradi — ya'ni yuqoridagi "saqlanmagan o'zgarish" himoyasi bu yerda ham ishlaydi. JavaScript'dan ham o'qiladi va yoziladi: dialog.closedBy = "any".

closedby hali Safari'da yo'q (web-features, 2026-oktabr). U yerda ham ishlashi kerak bo'lsa — eski usul: oynaning o'ziga bosilganini tekshirish. ::backdrop ga bosilganda hodisa target i — dialog ning o'zi, ichidagi kontentga bosilganda esa — ichki element:

js
dialog.addEventListener("click", (event) => {
  if (event.target === dialog) dialog.requestClose(); // fonga bosildi
});

Bu ishlashi uchun oynaning padding i 0 bo'lishi va kontent ichki <div> da turishi kerak — aks holda oyna chetidagi bo'sh joyga bosish ham "tashqari" hisoblanadi.

4. Fokus va inert

4.1 Fokus qaytishi

Modal yopilganda brauzer fokusni oynani ochgan elementga o'zi qaytaradi — yuqoridagi misolda shuni ko'rdik. Lekin ochgan element o'chib ketgan bo'lsa-chi? Masalan, ro'yxat qatoridagi «O'chirish» tugmasi oyna ochiq turganda qayta chizildi. Unda fokus <body> ga tushadi va Bobur sahifaning boshidan qayta Tab bosishga majbur. Bu holda fokusni o'zingiz qaytarasiz — "Vazifalar qadami"da aynan shu bor.

4.2 inert: "uxlab turgan" qism

showModal() sahifaning qolgan qismini avtomatik inert qiladi: u yerga bosish, Tab bilan o'tish va ekran o'quvchida o'qish mumkin emas. Biz buni Dinamik UI'da qulaylik darsida inert atributi bilan qo'lda ham qilgan edik:

js
const main = document.querySelector("main");

main.inert = true; // yuklanayotganda: bosish va Tab yo'q
await saveBooking();
main.inert = false;

Qachon qo'lda kerak? Modal emas, lekin "fon to'xtasin" kerak bo'lgan holatlarda: yon panel (drawer) ochiq, ma'lumot yuklanmoqda, bir qismi vaqtincha ishlamaydi. Modal oyna uchun esa — faqat showModal(), inert ni qo'lda qo'yish shart emas.

5. Popover JavaScript'dan

5.1 Uch metod

js
menu.showPopover(); // ochish
menu.hidePopover(); // yopish
menu.togglePopover(); // almashtirish; true — ochildi, false — yopildi
menu.togglePopover(true); // faqat ochish (ochiq bo'lsa — o'zgarmaydi)
console.log(menu.matches(":popover-open")); // ochiqmi

Bu metodlar popover atributi bor elementda ishlaydi. Oddiy <div> da: NotSupportedError: Failed to execute 'showPopover' on 'HTMLElement': Not supported on elements that are not popovers. Atributni JavaScript'dan ham qo'yish mumkin: element.popover = "manual".

Ochiq popover'ga yana showPopover() yoki yopiqqa hidePopover() — xato bermaydi (Chrome 154 da sinadik), shunchaki hech narsa o'zgarmaydi.

5.2 Turlar JavaScript'dan qaraganda

Popover API darsidagi uch tur metodlar bilan ham bir xil ishlaydi. Chrome 154 da tekshirdik:

  • auto popover ochiq turganda boshqa auto ochilsa — birinchisi o'zi yopiladi (ichma-ich bo'lmasa).
  • manual — boshqalar ochilganda ham, tashqariga bosilganda ham, Esc da ham ochiq qoladi. Faqat hidePopover() yoki tugma yopadi.
  • hint — kichik maslahat (tooltip) uchun: ochiq auto menyuni yopmaydi, lekin tashqariga bosilganda o'zi yopiladi.

5.3 beforetoggle va toggle

Ikki hodisa:

  • beforetoggle — holat o'zgarishidan oldin. Ochilayotganda uni preventDefault() bilan to'xtatish mumkin.
  • toggle — o'zgarishdan keyin. Klaviatura hodisalari darsida portfolio menyusida uni kuzatgan edingiz.

Ikkalasida ham oldState va newState bor: "closed" yoki "open".

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
  [popover] { border: 1px solid #c9d6cf; border-radius: 0.5rem;
    padding: 0.75rem 1rem; }
</style>
<button id="cart-button" type="button">
  Savat (<span id="count">0</span>)</button>
<button id="add" type="button">Osh qo'shish</button>
<div id="cart" popover>
  <strong>Savat</strong>
  <ul id="items"></ul>
</div>
<script>
  const cart = document.getElementById("cart");
  const items = [];

  document.getElementById("add").addEventListener("click", () => {
    items.push("Osh — 35 000 so'm");
    document.getElementById("count").textContent = items.length;
  });

  const cartButton = document.getElementById("cart-button");
  cartButton.addEventListener("click", () => {
    cart.togglePopover();
  });

  cart.addEventListener("beforetoggle", (event) => {
    if (event.newState === "open" && items.length === 0) {
      event.preventDefault(); // bo'sh savat ochilmaydi
      console.log("savat bo'sh — ochilmadi");
      return;
    }
    if (event.newState === "open") {
      document.getElementById("items").replaceChildren(
        ...items.map((text) => {
          const li = document.createElement("li");
          li.textContent = text;
          return li;
        }),
      );
    }
  });

  cart.addEventListener("toggle", (event) => {
    console.log(`toggle: ${event.oldState} → ${event.newState}`);
  });
</script>

Avval «Savat (0)» ni bosing — konsolda savat bo'sh — ochilmadi, popover chiqmaydi. «Osh qo'shish», keyin «Savat (1)» — ro'yxat chizilib ochiladi: toggle: closed → open. Tashqariga bosing — toggle: open → closed. Biz shu tartibda bosib tekshirdik.

Ro'yxat beforetoggle da chiziladi — popover ko'rinishidan oldin, foydalanuvchi eski ro'yxatni bir lahza ham ko'rmaydi.

5.4 toggle — kechikadi va birlashadi

Bitta nozik joy: toggle hodisasi darhol emas, keyinroq — alohida task bo'lib (Microtask va macrotask darsidagi navbat orqali) keladi va ketma-ket o'zgarishlar birlashadi. Chrome 154 da sinadik: popover'ni bir funksiya ichida to'rt marta ochib-yopdik (show, hide, show, hide). beforetoggle har safar keldi, toggle esa bir marta: closed → closed. Brauzer "boshida yopiq edi, oxirida ham yopiq" deb xulosa qildi.

Qoida: "hozir ochildi — darhol nimadir qil" kerak bo'lsa (masalan, ichidagi ro'yxatni chizish) — beforetoggle. "Yakuniy holat nima bo'ldi" (analitika, aria-expanded ni yangilash) — toggle.

5.5 source: popover'ni tugmaga bog'lash

popovertarget li tugma ochgan popover brauzer uchun shu tugmaga bog'langan: ekran o'quvchi "menyu tugmasi — kengaytirilgan" deydi, popover yopilganda fokus tugmaga qaytadi. JavaScript'dan ochganda bu bog'liqlik yo'q. Uni source bilan berasiz:

js
menu.showPopover({ source: menuButton });
menu.togglePopover({ source: menuButton });

Chrome 154 da bu sintaksis xatosiz ishladi. Imkon bo'lsa, oddiy holatda popovertarget (yoki commandfor) li tugmani ishlating — bog'lanish o'z-o'zidan bo'ladi. JavaScript — qo'shimcha mantiq uchun.

6. Invoker commands va command hodisasi

Invoker commands darsida commandfor + command tugmalarini ko'rdik. Har bosishda nishon elementga command hodisasi keladi — hatto o'rnatilgan buyruqlarda ham:

js
dialog.addEventListener("command", (event) => {
  console.log(event.command); // "show-modal", "close", "--yangila"
  console.log(event.source); // bosilgan tugma
  if (event.command === "--yangila") refreshList();
});

-- bilan boshlanadigan nom — o'z buyrug'ingiz: brauzer unga hech narsa qilmaydi, faqat hodisani yetkazadi. Chrome 154 da sinadik: show-modal, --yangila, close, request-close — to'rttasida ham CommandEvent keldi, event.source — bosilgan tugma.

7. Ko'p uchraydigan xatolar

7.1 returnValue ni tozalamaslik

Eski javob keyingi Esc da "qaytadi". Tuzatish: har showModal() dan oldin dialog.returnValue = "".

7.2 Tinglovchilar to'planishi

close tinglovchisi har ochishda once siz qo'shiladi — bir yopilishda ko'p chaqiruv. Tuzatish: { once: true } yoki tinglovchini bir marta, funksiyadan tashqarida qo'shing.

7.3 id="open" va id="close"

Bu darsni yozayotganda biz o'zimiz qoqildik. Tugmaga id="open" berib, JavaScript'da qisqa yo'l bilan open.onclick = ... yozdik — hech narsa ishlamadi. Sabab: brauzer id li elementni global nom qiladi, lekin window da shu nomli xususiyat bo'lsa — u ustun. open — bu window.open funksiyasi, close — window.close, opener — window.opener (null, va opener.onclick TypeError beradi). Tuzatish: elementni doim getElementById bilan oling; id ga open, close, name, status kabi nom bermang.

7.4 Ochishda cancel ni doim to'xtatish

Esc ishlamaydigan, chiqish tugmasi yo'q oyna — klaviatura foydalanuvchisi uchun tuzoq. Tuzatish: preventDefault() faqat aniq sabab bilan va oynada chiqish yo'li ko'rinib tursin.

7.5 Xabar uchun modal

"Saqlandi " kabi xabar uchun showModal() — foydalanuvchi ishini to'xtatib, uni bosishga majburlaydi. Tuzatish: javob kerak bo'lmagan xabar — manual popover yoki role="status" (2-mashq).

8. Mashqlar

1-mashq (oson): Porsiya tanlash

«Osh» kartasida «Buyurtma» tugmasi modal oyna ochsin: «Necha porsiya?» va uch tugma — 1, 2, 3 (form method="dialog", value bilan). Yopilganda tanlangan son #order ga yozilsin: Osh × 2. Esc bilan yopilsa — Buyurtma berilmadi. Konsolga returnValue ni JSON.stringify bilan chiqaring.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.3rem 0.8rem; }
</style>
<button id="order-button" type="button">Buyurtma</button>
<p id="order"></p>
<dialog id="portions" aria-labelledby="portions-title">
  <form method="dialog">
    <p id="portions-title">Necha porsiya?</p>
    <button value="1">1</button>
    <button value="2">2</button>
    <button value="3">3</button>
  </form>
</dialog>
<script>
  const portions = document.getElementById("portions");
  const order = document.getElementById("order");

  const orderButton = document.getElementById("order-button");
  orderButton.addEventListener("click", () => {
    portions.returnValue = "";
    portions.showModal();
  });

  portions.addEventListener("close", () => {
    const value = portions.returnValue;
    console.log(JSON.stringify(value));
    order.textContent = value
      ? `Osh × ${value}`
      : "Buyurtma berilmadi";
  });
</script>

«Buyurtma» → 2 — konsolda "2", sahifada Osh × 2. Yana oching va Esc — "", Buyurtma berilmadi. Biz ikkala yo'lni sinadik. returnValue = "" qatorini olib tashlab sinang: Esc dan keyin ham Osh × 2 chiqadi — eski javob qolib ketdi.

2-mashq (o'rta): Toast xabarlar

showToast(text) funksiyasini yozing: ekranning pastki o'ng burchagida manual popover chiqsin va 3 soniyadan keyin o'zi yopilib, DOM'dan o'chsin. Bir nechta toast ketma-ket chiqsa — ular ustma-ust emas, bir-birining ustida (ustun bo'lib) tursin. Har toast role="status". «Bron» tugmasi showToast("Bron qabul qilindi: 6-stol") ni chaqirsin, konsolga ochiq toastlar soni chiqsin.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem;
    min-height: 12rem; }
  .toast { inset: auto 1rem 1rem auto; margin: 0;
    padding: 0.6rem 1rem; border: 0; border-radius: 0.5rem;
    background: #1f6f4a; color: #fff; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="book" type="button">Bron</button>
<script>
  const GAP = 8;

  function placeToasts() {
    let bottom = 16;
    const open = document.querySelectorAll(".toast:popover-open");
    for (const toast of [...open].reverse()) {
      toast.style.bottom = `${bottom}px`;
      bottom += toast.offsetHeight + GAP;
    }
  }

  function showToast(text) {
    const toast = document.createElement("div");
    toast.className = "toast";
    toast.popover = "manual";
    toast.role = "status";
    toast.textContent = text;
    document.body.append(toast);
    toast.showPopover();
    placeToasts();
    setTimeout(() => {
      toast.hidePopover();
      toast.remove();
      placeToasts();
    }, 3000);
  }

  document.getElementById("book").addEventListener("click", () => {
    showToast("Bron qabul qilindi: 6-stol");
    const count = document.querySelectorAll(".toast:popover-open");
    console.log(`ochiq toastlar: ${count.length}`);
  });
</script>

«Bron» ni uch marta tez bosing — uch toast ustun bo'lib chiqadi, konsolda ochiq toastlar: 1, 2, 3. Eng yangisi — eng pastda. 3 soniyadan keyin eng birinchisi (eng tepadagisi) yo'qoladi, keyin qolganlari navbat bilan. Biz sinadik: uch bosishdan keyin uchta ochiq toast, 3,5 soniyadan keyin — nol.

manual — ataylab: auto bo'lsa, ikkinchi toast birinchisini yopardi va tashqariga bosish ularni yo'qotardi. inset: auto 1rem 1rem auto — popover'ning sukut bo'yicha markazdagi joyini burchakka ko'chiradi. role="status" — Malika xabarni eshitadi.

3-mashq (qiyin): Umumiy askConfirm

Darsdagi askConfirm faqat bitta oynaga bog'langan. Uni umumiy qiling: askConfirm({ title, text, confirmText }) har chaqiruvda yangi <dialog> yaratsin (sarlavha, matn, «Bekor qilish» autofocus bilan va tasdiq tugmasi), Promise qaytarsin va yopilgach oynani DOM'dan o'chirsin. Hech qanday matn innerHTML orqali qo'yilmasin. Sinov: «Menyudan o'chirish» tugmasi askConfirm({ title: "Lag'mon menyudan olinsinmi?", text: "Mehmonlar uni ko'rmaydi.", confirmText: "Olib tashlash" }) ni chaqirsin va konsolga javobni yozsin.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="remove" type="button">Menyudan o'chirish</button>
<script>
  let nextId = 1;

  function askConfirm({ title, text, confirmText }) {
    const dialog = document.createElement("dialog");
    const titleId = `confirm-title-${nextId++}`;
    dialog.setAttribute("aria-labelledby", titleId);

    const form = document.createElement("form");
    form.method = "dialog";
    const heading = document.createElement("h2");
    heading.id = titleId;
    heading.textContent = title;
    const body = document.createElement("p");
    body.textContent = text;
    const cancel = document.createElement("button");
    cancel.value = "cancel";
    cancel.autofocus = true;
    cancel.textContent = "Bekor qilish";
    const confirm = document.createElement("button");
    confirm.value = "confirm";
    confirm.textContent = confirmText;

    form.append(heading, body, cancel, confirm);
    dialog.append(form);
    document.body.append(dialog);
    dialog.showModal();

    return new Promise((resolve) => {
      dialog.addEventListener("close", () => {
        resolve(dialog.returnValue === "confirm");
        dialog.remove();
      }, { once: true });
    });
  }

  const removeButton = document.getElementById("remove");
  removeButton.addEventListener("click", async () => {
    const yes = await askConfirm({
      title: "Lag'mon menyudan olinsinmi?",
      text: "Mehmonlar uni ko'rmaydi.",
      confirmText: "Olib tashlash",
    });
    console.log(`javob: ${yes}`);
  });
</script>

«Menyudan o'chirish» → «Olib tashlash» — javob: true; «Bekor qilish» yoki Esc — javob: false. Har safar yopilgach, sahifada dialog qolmaydi. Biz uchala yo'lni va document.querySelectorAll("dialog").length === 0 ni tekshirdik.

Har oynada yangi oyna bo'lgani uchun returnValue ni tozalash shart emas — u har doim bo'sh boshlanadi. aria-labelledby uchun noyob id — sahifada ikki oyna bir vaqtda bo'lsa ham to'qnashmasin. Matnlar textContent bilan: sarlavhaga <img onerror=…> kelib qolsa ham, u matn bo'lib chiqadi (Kontentni o'zgartirish va XSS xavfi).

4-mashq: Vazifalar qadami — o'chirishni tasdiqlash

vazifalar dagi «O'chirish» tugmasi hozir vazifani darhol o'chiradi. Tasodifiy bosish — yo'qolgan vazifa. Endi u modal oyna bilan so'raydi. Bu — darsdagi askConfirm naqshi, kanon nomlari bilan.

1. Branch:

bash
git switch -c feature/ochirish-dialog

2. index.html — import xatosi qatoridan keyin, tema guruhidan oldin:

html
<dialog id="ochirish-dialog" aria-labelledby="ochirish-sarlavha"
        aria-describedby="ochirish-matn">
  <form method="dialog">
    <h2 id="ochirish-sarlavha">Vazifa o'chirilsinmi?</h2>
    <p id="ochirish-matn"></p>
    <div class="qator">
      <button value="bekor" autofocus>Bekor qilish</button>
      <button value="ochir" class="xavfli">O'chirish</button>
    </div>
  </form>
</dialog>

aria-describedby — ekran o'quvchi sarlavhadan keyin qaysi vazifa ekanini ham o'qiydi. autofocus «Bekor qilish» da — xavfli amalda xavfsiz standart.

3. render.js — oynani boshqaradigan funksiya (DOM'ga tegadigan hamma narsa shu modulda):

js
const ochirishDialog = document.getElementById("ochirish-dialog");
const ochirishMatn = document.getElementById("ochirish-matn");

// Modal oyna: fon inert bo'ladi, fokus ichida aylanadi, Esc yopadi.
// Javob — Promise: true faqat «O'chirish» bosilganda (Esc → "")
export function ochirishniSora(matn) {
  ochirishMatn.textContent = `«${matn}» ro'yxatdan o'chiriladi.`;
  ochirishDialog.returnValue = "";
  ochirishDialog.showModal();
  return new Promise((hal) => {
    ochirishDialog.addEventListener(
      "close",
      () => hal(ochirishDialog.returnValue === "ochir"),
      { once: true },
    );
  });
}

Darsdagi askConfirm bilan solishtiring: bir xil. Vazifa matni textContent bilan — foydalanuvchi yozgan matn HTML bo'lib bajarilmaydi.

4. asosiy.js — yangi funksiya va bitta case:

js
// Dialog ochiq turganda vazifani boshqa tab yoki server o'chirgan
// bo'lishi mumkin — javobdan keyin qayta tekshiramiz. Fokusni o'zimiz
// qaytaramiz: fon yangilangan bo'lsa, dialog eslagan tugma endi yo'q
async function ochirishniSorab(id, zaxira) {
  const tasdiq = await ochirishniSora(vazifaniTop(id).matn);
  if (tasdiq && vazifaniTop(id)) {
    qatorniOchir(id, zaxira);
  } else {
    fokusla(id, "ochir", zaxira);
  }
}

// royxat click tinglovchisida:
    case "ochir":
      ochirishniSorab(id, zaxira); // oldin: qatorniOchir(id, zaxira)
      break;

Ikki nozik joy — ikkalasi ham bu darsdan:

  • vazifaniTop(id) qayta tekshiriladi. await paytida dunyo o'zgarishi mumkin: oyna ochiq turganda boshqa tab (BroadcastChannel) yoki server shu vazifani o'chirgan bo'lishi mumkin. Javob kelgach "vazifa hali bormi?" deb so'raymiz.
  • Fokus o'zimiz qaytaramiz ("Fokus qaytishi" bo'limi). Oyna ochiq paytda ro'yxat fonda qayta chizilsa, oynani ochgan tugma DOM'da yo'q — brauzer fokusni qaytara olmaydi. fokusla yangi chizilgan qatordagi «O'chirish» ni topadi.

5. vazifalar.css:

css
dialog {
  max-width: min(28rem, calc(100% - 2rem));
  padding: 1rem 1.25rem;
  border: 1px solid var(--rang-chegara);
  border-radius: 0.75rem;
  background: var(--rang-yuza);
  color: var(--rang-matn);
}
dialog::backdrop { background: rgb(0 0 0 / 0.5); }
dialog h2 { margin-top: 0; }
dialog[open] { animation: dialog-kirish 0.15s ease-out; }
@keyframes dialog-kirish {
  from { opacity: 0; transform: translateY(0.5rem); }
}
.xavfli { border-color: var(--rang-xato); color: var(--rang-xato); }

Ranglar — matchMedia darsidagi tema tokenlari: oyna qorong'i rejimda ham to'g'ri. Kirish animatsiyasini o'tgan qadamdagi prefers-reduced-motion qoidasi o'zi o'chiradi — yangi kod shart emas.

6. Testlar. Yangi sof mantiq yo'q — node:test natijasi o'zgarmaydi: 70/70. Brauzer tekshiruvida esa o'chirishning ikki eski qadami endi oynani tasdiqlashi kerak — yordamchi ochirishniTasdiqla qo'shildi. Bir nozik joy: close hodisasi alohida task bo'lib keladi, shuning uchun tekshiruv tugmani bosgach e'lon matni chiqishini kutadi.

7. Brauzerda (kanon tekshiruvi, 69/69 ning bir qismi):

text
✅ dialog: Enter bilan ochildi, modal: [true,true]
✅ dialog: fokus «Bekor qilish» da (autofocus): "dialog:bekor"
✅ dialog: Esc — yopildi, o'chmadi, fokus tugmaga qaytdi: [false,6,"ochir7"]
✅ dialog: Tab → «O'chirish»: "dialog:ochir"
✅ dialog: tasdiq — e'lon: "«Dialog sinovi» o'chirildi."
✅ dialog: o'chdi (ekran va server): [5,false]

Hammasi klaviatura bilan — Bobur sinovi: Enter oynani ochdi, fokus darhol «Bekor qilish» da, Esc hech narsani o'chirmadi va fokusni o'sha qatorning tugmasiga qaytardi. Tab — «O'chirish», Enter — vazifa ekrandan ham, serverdan ham o'chdi.

O'zingiz: Live Server'da vazifa qo'shing, «O'chirish» ni bosing va uchala yo'lni sinang. DevTools Elements panelida oyna ochiq paytda #top-layer belgisini ko'ring.

8. Commit va PR. To'liq xabar 76 belgi — Git sarlavhasi uchun uzun (≤ 72). Shuning uchun sarlavha va tanaga bo'lamiz:

bash
git add index.html assets/js/render.js assets/js/asosiy.js \
  assets/css/vazifalar.css
git commit -m "feat: o'chirish <dialog> bilan tasdiqlanadi" \
  -m "klaviatura, Esc, fokus qaytadi"
git push -u origin feature/ochirish-dialog
gh pr create --fill
gh pr merge --merge

Diff: 4 fayl, +57 −1. Xulq ataylab o'zgardi: «O'chirish» endi so'raydi.

9. Real ishda

  • Tasdiqlash oynalari — GitHub ("Delete repository"), Telegram Web ("Chatni o'chirish"), internet-banklar. Ko'p UI kutubxonalari (Radix, Headless UI, shadcn/ui) bugun ichida <dialog> yoki uning qoidalarini takrorlaydi.
  • askConfirm naqshi — "Promise qaytaradigan modal" React'da ham keng tarqalgan (React — 17-qismda).
  • Toast, menyu, maslahat — popover API'ning tabiiy joyi. Ilgari ular uchun z-index urushlari va o'nlab qator JavaScript yozilardi; top layer bu muammoni yo'qotdi.
  • Accessibility auditi — "modal fokusni ushlaydimi, Esc yopadimi, fokus qaytadimi?" — har auditda tekshiriladi.
  • Intervyu. "confirm() o'rniga nima ishlatasiz?", "close va cancel farqi?", "Popover va dialog farqi?" — frontend intervyularida uchraydi.

Xulosa

  • showModal() — modal (top layer, inert fon, Esc); show() / open — non-modal; close(qiymat) — yopish va returnValue.
  • returnValue oldingi ochilishdan qoladi — har ochishdan oldin "".
  • cancel — Esc, tashqariga bosish va requestClose() da, close dan oldin, to'xtatsa bo'ladi; close — har qanday yopilishda.
  • Tasdiqlash — Promise qaytaradigan funksiya, { once: true } tinglovchi bilan.
  • Popover: showPopover, hidePopover, togglePopover; beforetoggle — oldin va to'xtatsa bo'ladi, toggle — keyin va birlashadi.
  • Fokus qaytishini tekshiring; ochgan element yo'qolsa — o'zingiz qaytaring.

Keyingi dars: Web Animations API — animatsiyalarni JavaScript'dan yaratish, to'xtatish va boshqarish.

Manbalar

  • WHATWG: "HTML Standard" — The dialog element, The popover attribute — html.spec.whatwg.org
  • MDN: HTMLDialogElement, Popover API, ToggleEvent, CommandEvent — developer.mozilla.org
  • Chrome for Developers: "dialog.requestClose()" va "Invoker commands" — developer.chrome.com
  • web.dev: "Building a dialog component" — web.dev
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript <dialog> va Popover API: modal oyna va popover'ni JS'dan boshqarish — IlmHamroh