IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya18/30-dars20 daqiqa
Mundarija (27)

Resizable panellar, split view va masonry React'da: ajratkich, layoutni saqlash va ResizeObserver

Qisqacha: Split view — ekran ikki-uch panelga bo'linadi, ular orasidagi ajratkichni sudrab o'lchamni o'zgartirasiz (pochta: ro'yxat | xat). A11y uchun ajratkich — role="separator", fokus oladi, aria-valuenow bilan foizni aytadi va strelkalar bilan suriladi (WAI-ARIA Window Splitter). Tayyor yo'l — react-resizable-panels (v4: Group, Panel, Separator; v3 dagi PanelGroup — eski kodda), layout useDefaultLayout bilan saqlanadi. Masonry — har xil balandlikdagi kartalar ustunlarda zich terilgan to'r: CSS columns (o'qish tartibi ustun bo'yicha) yoki JavaScript (masonic — qisqa ustunga qo'yish, tartib qator bo'yicha). O'lchamni kuzatish — ResizeObserver. Kanon ularning hech birini olmadi: vazifalar — 36rem bitta ustun, panellar +13.1 KB bo'lardi, foydasi yo'q.

Bu darsda

  • WAI-ARIA naqshi bo'yicha o'z ajratkichingizni yozasiz: sichqoncha va klaviatura bilan.
  • Panel o'lchamini localStorage da saqlab, qayta ochilganda tiklaysiz.
  • react-resizable-panels v4 API'sini o'qiysiz va v3 kodini taniysiz.
  • Masonry'ning ikki yo'lini (CSS columns va JavaScript) o'qish tartibi bilan solishtirasiz.
  • ResizeObserver bilan element enini React'da kuzatasiz.
  • Bu vositalar nega vazifalar ga kirmaganini o'lchov bilan tushuntirasiz.

Oldin bilishingiz kerak: Drag and drop: dnd-kit, ResizeObserver va MutationObserver, Layout naqshlari: masonry, Container queries, Fokus boshqaruvi.

1. Nega bu kerak?

O'tgan darsda qatorlarni sudrab tartibladik. Sudrashning yana bir turi bor — o'lchamni sudrash.

«Bahor» admin panelining kompyuter versiyasi uchun Jasur aka shunday dedi: "Chapda buyurtmalar ro'yxati, o'ngda tanlangan buyurtma tafsiloti. Ba'zan ro'yxatni keng ko'rmoqchiman, ba'zan tafsilotni — o'rtadagi chiziqni o'zim suray." Bu — split view (bo'lingan ko'rinish): VS Code, Telegram Desktop, pochta dasturlari aynan shunday.

Ikkinchi so'rov — "Mijozlar fikrlari" sahifasi. Fikrlarning ba'zisi bir qator, ba'zisi o'n qator. Oddiy to'rda (grid) qisqa kartalar ostida bo'sh joy qoladi. Pinterest kabi zich terish kerak — masonry.

Hayotiy o'xshatish: kitob javoni va g'isht devor. Javon ichidagi to'siqni surib, bir bo'limni kengaytirib, ikkinchisini toraytirasiz — bu split view. G'isht teruvchi esa har g'ishtni eng past joyga qo'yadi, shunda devor tekis o'sadi — bu masonry ("mason" — inglizcha g'isht teruvchi).

2. O'z ajratkichingiz

2.1 WAI-ARIA Window Splitter

Ajratkich ko'rinishda — oddiy chiziq. Lekin Bobur uni Tab bilan topishi, Malika esa "panel o'lchami, 40 foiz" ni eshitishi kerak. WAI-ARIA APG'da buning tayyor naqshi bor — Window Splitter:

Atribut / tugma Ma'nosi
role="separator" + tabIndex={0} fokus oladigan ajratkich ("bo'luvchi")
aria-valuenow, aria-valuemin, aria-valuemax asosiy panelning hozirgi o'lchami va chegaralari (odatda 0–100)
aria-controls qaysi panel o'zgaradi
aria-orientation="vertical" ajratkich tik (panellar yonma-yon)
← / → ajratkichni surish
Home / End eng kichik / eng katta o'lcham
Enter asosiy panelni yig'ish yoki qaytarish

Nima uchun aria-orientation="vertical", panellar esa gorizontal joylashgan? Chunki ajratkich chizig'i tik. ARIA chiziqning yo'nalishini so'raydi, uning harakat yo'nalishini emas.

2.2 Jonli misol

Chap panel o'lchami — bitta state, foizda. Klaviatura va sichqoncha ikkalasi ham shu sonni o'zgartiradi. «Ishga tushir» dan keyin Tab bilan ajratkichga keling va strelkalarni bosing; sichqoncha bilan sudrab ham ko'ring:

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

const MIN = 20;
const MAX = 80;
const STEP = 5;
const clamp = (v) => Math.min(MAX, Math.max(MIN, Math.round(v)));

export default function App() {
  const [left, setLeft] = useState(40); // chap panel, foiz
  const box = useRef(null);
  useEffect(() => {
    console.log(`chap panel: ${left}%`);
  }, [left]);

  function onKeyDown(e) {
    const keys = {
      ArrowLeft: left - STEP,
      ArrowRight: left + STEP,
      Home: MIN,
      End: MAX,
    };
    if (!(e.key in keys)) return;
    e.preventDefault(); // sahifa aylanmasin
    setLeft(clamp(keys[e.key]));
  }

  function onPointerDown(e) {
    const handle = e.currentTarget;
    handle.setPointerCapture(e.pointerId);
    const rect = box.current.getBoundingClientRect();
    const move = (ev) =>
      setLeft(clamp(((ev.clientX - rect.left) / rect.width) * 100));
    const up = () => {
      handle.removeEventListener("pointermove", move);
      handle.removeEventListener("pointerup", up);
    };
    handle.addEventListener("pointermove", move);
    handle.addEventListener("pointerup", up);
  }

  return (
    <div ref={box} style={{ display: "flex", height: 120 }}>
      <section id="orders" style={{ flexBasis: `${left}%` }}>
        Buyurtmalar
      </section>
      <div
        role="separator"
        tabIndex={0}
        aria-label="Panel o'lchami"
        aria-controls="orders"
        aria-orientation="vertical"
        aria-valuenow={left}
        aria-valuemin={MIN}
        aria-valuemax={MAX}
        onKeyDown={onKeyDown}
        onPointerDown={onPointerDown}
        style={{
          width: 8,
          cursor: "col-resize",
          touchAction: "none",
          background: "#99a",
        }}
      />
      <section style={{ flex: 1 }}>Tafsilot</section>
    </div>
  );
}

Konsolda:

text
chap panel: 40%
chap panel: 45%
chap panel: 50%
chap panel: 45%

Ekranda:

text
Buyurtmalar
Tafsilot

Klaviatura yo'li hamma joyda bir xil ishlaydi — u faqat sonni o'zgartiradi. Sichqoncha yo'li esa konteynerning haqiqiy enini o'lchaydi (getBoundingClientRect) — u faqat brauzerda ishlaydi. Biz Chrome 155 da ajratkichni sichqoncha bilan 120 px o'ngga surdik (konteyner eni 584 px). Konsolda:

text
chap panel: 40%
chap panel: 43%
chap panel: 45%
…
chap panel: 59%
chap panel: 61%

Sichqoncha 10 qadamda harakatlandi — har pointermove da yangi qiymat (o'rtadagi qatorlar qisqartirildi). Hisob: 120 ÷ 584 ≈ 20.5%, ya'ni 40 + 20.5 ≈ 61. aria-valuenow ham 61 bo'ldi.

Kodning to'rt muhim joyi:

  • e.preventDefault() — strelkalar odatda sahifani aylantiradi. Ajratkich fokusda bo'lsa, ular faqat panelni o'zgartirsin.
  • clamp — 20% dan kichik, 80% dan katta bo'lmasin. Math.round — aria-valuenow butun son bo'lsin: Malika "43,7" emas, "44" ni eshitsin.
  • setPointerCapture — sichqoncha ajratkichdan chiqib ketsa ham harakat unga keladi (Barmoq bilan surish darsidagi usul). touch-action: none — telefonda barmoq bilan surish sahifani aylantirmasin.
  • flexBasis foizda — o'lcham konteyner eniga nisbatan. Oyna kengaysa, panellar nisbati saqlanadi.

Tekshirib ko'ring: Ajratkichni <div> emas, <button> qilsak bo'lmaydimi? U ham fokus oladi.

Javob

<button> ning roli — tugma: Malika "Panel o'lchami, tugma" ni eshitadi va uni bosishni kutadi. Lekin ajratkich bosilmaydi — u qiymatga ega (40 foiz) va strelkalar bilan o'zgaradi. role="separator" (fokus oladigan, aria-valuenow li) aynan shu ma'noni beradi: "o'lchami o'zgaradigan bo'luvchi, hozir 40". Ekran o'quvchi ham strelkalar unga tegishli ekanini biladi.

2.3 Layoutni saqlash

Jasur aka ro'yxatni 60% ga kengaytirdi, sahifani yangiladi — yana 40%. Foydalanuvchi tanlagan layout saqlanishi kerak. Bu shaxsiy qulaylik — localStorage yetadi (localStorage va sessionStorage darsi). Server va URL — ortiqcha: layout ulashilmaydi.

Natija oynasida localStorage bor (sahifa yangilanganda tozalanadi). Panelni yopib-ochish bilan tekshiramiz:

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

const KEY = "bahor:panel:orders";

function readSize() {
  const saved = Number(localStorage.getItem(KEY));
  // noto'g'ri yoki bo'sh qiymat — standart
  return saved >= 20 && saved <= 80 ? saved : 40;
}

function SplitView() {
  const [left, setLeft] = useState(readSize);
  useEffect(() => {
    console.log("tiklandi:", readSize());
  }, []);
  useEffect(() => {
    localStorage.setItem(KEY, String(left));
  }, [left]);
  return (
    <div>
      <p>Buyurtmalar: {left}%</p>
      <button onClick={() => setLeft(Math.min(80, left + 20))}>
        Kengaytirish
      </button>
    </div>
  );
}

export default function App() {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>
        {open ? "Yopish" : "Ochish"}
      </button>
      {open && <SplitView />}
    </div>
  );
}

Konsolda:

text
tiklandi: 40
tiklandi: 60

Ekranda:

text
Yopish
Buyurtmalar: 60%
Kengaytirish

Panel yopilib qayta ochilganda (komponent yangidan yaratildi) o'lcham localStorage dan tiklandi — 60%. Uch nozik joy:

  • useState(readSize) — funksiyaning o'zi beriladi, chaqiruvi emas (useState darsidagi dangasa boshlang'ich qiymat): localStorage faqat birinchi renderda o'qiladi.
  • Tekshirish — saqlangan qiymat buzilgan bo'lishi mumkin (eski versiya, qo'lda tahrir). Chegaradan tashqarida — standart.
  • Kalitda ilova nomi — bahor:panel:orders. Bir domendagi boshqa sahifalar bilan to'qnashmasin.

3. react-resizable-panels

3.1 v4 API

O'z ajratkichingiz ikki panel uchun yetadi. Uch-to'rt panel, ichma-ich guruhlar (chapda ro'yxat, o'ngda tepa-past bo'lingan tafsilot), yig'iladigan panel, minimal o'lcham pikselda — bularning hammasi react-resizable-panels da tayyor. U shadcn/ui'ning Resizable komponenti asosi.

Kanon tajribasidagi versiya — 4.14.3. 4-versiya (2025-yil dekabr) nomlarni o'zgartirgan:

jsx
import {
  Group,
  Panel,
  Separator,
  useDefaultLayout,
} from "react-resizable-panels";

function OrdersScreen() {
  // layoutni localStorage'da saqlash va tiklash
  const layout = useDefaultLayout({ id: "bahor-orders" });
  return (
    <Group
      orientation="horizontal"
      defaultLayout={layout.defaultLayout}
      onLayoutChanged={layout.onLayoutChanged}
    >
      <Panel id="list" defaultSize="40" minSize="20" collapsible>
        <OrderList />
      </Panel>
      <Separator />
      <Panel id="details" minSize={240}>
        <OrderDetails />
      </Panel>
    </Group>
  );
}

O'qiymiz:

  • Group — panellar guruhi, orientation — yonma-yon ("horizontal") yoki ustma-ust.
  • Panel — o'lcham qoidalari: defaultSize, minSize, maxSize, collapsible. Birlikka e'tibor bering: son — piksel (minSize={240}), birliksiz satr — foiz ("40"), boshqa birlik aniq yoziladi ("20rem").
  • Separator — ajratkich. Hujjat uni "majburiy emas, lekin tavsiya" deydi: klaviatura a11y'si aynan undan keladi (biz yozgan role="separator" va ARIA xususiyatlari). Ajratkich ikki marta bosilsa, panel standart o'lchamiga qaytadi.
  • useDefaultLayout({ id }) — layoutni localStorage da saqlaydi va tiklaydi. onLayoutChanged sudrash tugagach chaqiriladi — har piksel harakatda saqlamaydi.

Panelni koddan boshqarish uchun panelRef bor: collapse(), expand(), resize(size), isCollapsed().

3.2 Eski kodda ko'rsangiz: v3

Internetdagi ko'p misollar (va shadcn'ning eski Resizable i) 3-versiyada:

v3 (eski) v4 (yangi)
PanelGroup direction="horizontal" Group orientation="horizontal"
PanelResizeHandle Separator
autoSaveId="…" useDefaultLayout({ id: "…" })
defaultSize={40} — foiz defaultSize="40" — foiz, {40} — piksel

Oxirgi qator — eng xavfli tuzoq: v3 dagi defaultSize={40} v4 da 40 piksel bo'lib qoladi. Kod xatosiz ishlaydi, lekin panel juda tor.

3.3 Biz sinab ko'rdik

Kanon tajribasi (tajriba/kitlar/src/k/panel.tsx, kanondan aynan, qatorlar o'qish uchun bo'lingan):

tsx
// #18 react-resizable-panels 4.14.3: ro'yxat | tafsilot
import { Group, Panel, Separator } from "react-resizable-panels";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("ilova")!).render(
  <main>
    <h1>Panel</h1>
    <Group orientation="horizontal" style={{ height: 300 }}>
      <Panel defaultSize={40}>Royxat</Panel>
      <Separator />
      <Panel>Tafsilot</Panel>
    </Group>
  </main>,
);

Natija (natijalar/tajriba/kutubxonalar.txt, aynan):

text
asos            JS   67719 (+   0.0 KB) CSS      0 | CSP 0 | xato: yo'q
panel           JS   81136 (+  13.1 KB) CSS      0 | CSP 0 | xato: yo'q

+13.1 KB gzip, CSP 0. E'tibor bering: tajriba kodida defaultSize={40} — v4 da bu 40 piksel. Tajriba faqat hajm va CSP'ni o'lchagani uchun bu muhim emas edi, lekin xuddi shu satr real ilovada yuqoridagi tuzoq bo'lardi.

4. Masonry

4.1 CSS yo'li va uning chegarasi

Layout naqshlari darsida masonry'ni CSS bilan qurganmiz: columns: 12rem va break-inside: avoid. Yangi display: grid-lanes ham bor, lekin Baseline emas — biz Chrome 155 da ham tekshirdik: CSS.supports("display", "grid-lanes") — false.

CSS columns ning bitta xususiyati React ilovasida muammo bo'ladi — tartib. Haqiqiy brauzerda raqamlarga qarang:

html
<style>
  body { font: 1rem/1.4 system-ui, sans-serif; margin: 1rem; }
  .masonry { columns: 3; column-gap: 0.5rem; }
  .card { break-inside: avoid; margin: 0 0 0.5rem; padding: 0.5rem;
    border-radius: 0.5rem; background: #e8eef8; }
</style>
<div class="masonry">
  <div class="card" style="height: 60px">1</div>
  <div class="card" style="height: 120px">2</div>
  <div class="card" style="height: 40px">3</div>
  <div class="card" style="height: 90px">4</div>
  <div class="card" style="height: 50px">5</div>
  <div class="card" style="height: 110px">6</div>
  <div class="card" style="height: 70px">7</div>
</div>

Biz Chrome 155 da kartalarning joyini o'lchadik: 1-ustunda — 1 va 2, 2-ustunda — 3, 4, 5, 3-ustunda — 6 va 7 (oyna eni boshqacha bo'lsa, taqsimot ham o'zgaradi). Multi-column matnni gazetadek ustun bo'yicha to'ldiradi: avval birinchi ustun pastgacha, keyin ikkinchisi. Fikrlar lentasida esa eng yangisi tepada, chapdan o'ngga o'qilishi kerak: 1, 2, 3 — qator bo'yicha. Cheksiz yuklashda bundan ham yomoni: yangi kartalar qo'shilganda hamma ustun qaytadan taqsimlanadi va eski kartalar boshqa ustunga sakraydi.

4.2 JavaScript yo'li: eng qisqa ustunga

JavaScript masonry'ning g'oyasi g'isht teruvchiniki: har yangi karta eng qisqa ustunga qo'yiladi. Kartalar tartibi qator bo'yicha saqlanadi, yangi kartalar eskilarini qimirlatmaydi. Balandliklar ma'lum bo'lsa (yoki o'lchangan bo'lsa), algoritm — oddiy sikl:

jsx
const HEIGHTS = [60, 120, 40, 90, 50, 110, 70];

function layout(heights, columnCount, gap = 8) {
  const columns = Array.from({ length: columnCount }, () => []);
  const tops = new Array(columnCount).fill(0);
  heights.forEach((h, i) => {
    // eng qisqa ustun (teng bo'lsa — chaprog'i)
    const col = tops.indexOf(Math.min(...tops));
    columns[col].push(i + 1);
    tops[col] += h + gap;
  });
  return { columns, tops };
}

const { columns, tops } = layout(HEIGHTS, 3);
columns.forEach((c, i) => {
  const list = c.join(", ");
  console.log(`${i + 1}-ustun: ${list} (balandligi ${tops[i]})`);
});

Konsolda:

text
1-ustun: 1, 5, 6 (balandligi 244)
2-ustun: 2, 7 (balandligi 206)
3-ustun: 3, 4 (balandligi 146)

Kartalar 1, 2, 3 — uchta ustunning tepasida, chapdan o'ngga. 4-karta eng qisqa ustunga (3-ustun, 40 px li karta ostiga) tushdi. 5 va 6 — o'sha paytda eng qisqa bo'lib qolgan 1-ustunga. Har karta qo'yilgan paytdagi eng past joyga tushadi — g'isht teruvchi kabi.

masonic (4.1.0) kutubxonasi aynan shuni qiladi, ustiga ikki narsa qo'shadi: kartalarni o'zi o'lchaydi (ResizeObserver bilan — rasm yuklanib balandlik o'zgarsa ham) va virtualizatsiya qiladi — 10 000 kartadan faqat ko'rinadiganlari DOM'da (virtualizatsiya):

tsx
// #18 masonic 4.1.0: masonry
import { Masonry } from "masonic";
import { createRoot } from "react-dom/client";
const items = Array.from({ length: 30 }, (_, i) => ({
  id: i,
  h: 40 + (i % 5) * 20,
}));
createRoot(document.getElementById("ilova")!).render(
  <main>
    <h1>Masonry</h1>
    <Masonry
      items={items}
      columnWidth={120}
      render={({ data }) => (
        <div style={{ height: data.h, background: "#ddd" }}>
          {data.id}
        </div>
      )}
    />
  </main>,
);

Bu — kanon tajribasi (tajriba/kitlar/src/k/masonic.tsx, kanondan aynan, qatorlar bo'lingan). columnWidth — ustunning eng kichik eni: ustunlar soni konteyner eniga qarab o'zi hisoblanadi. Natija:

text
masonic         JS   73377 (+   5.5 KB) CSS      0 | CSP 0 | xato: yo'q

+5.5 KB, CSP 0. masonic har kartani position: absolute bilan style orqali joylaydi (CSSOM) — CSP'ga mos.

Yo'l Tartib Narx Qachon
CSS columns ustun bo'yicha 0 KB statik galereya, tartib muhim emas
display: grid-lanes qator bo'yicha 0 KB kelajakda, Baseline bo'lgach
JS (masonic) qator bo'yicha +5.5 KB lenta, cheksiz yuklash, minglab karta

5. ResizeObserver React'da

5.1 Nega kerak

Media query butun oyna eniga qaraydi. Panel esa sudralganda oyna o'zgarmaydi — faqat panel eni o'zgaradi. Panel ichidagi jadval "torayganimni" qanday biladi? CSS'da — container queries (@container). JavaScript'da — ResizeObserver (ResizeObserver darsi): element o'lchami o'zgarganda xabar beradi. masonic ham ustunlar sonini shu bilan hisoblaydi.

5.2 useMeasure hook

O'tgan darsdagi qorovul hooki bilan bir xil naqsh: ref callback ichida kuzatuvchi, React 19 cleanup'ida uzish. Tekshiruv muhitida (jsdom) haqiqiy joylashuv yo'q — o'lchanadigan en ham yo'q, shuning uchun blok brauzerda ishlaydi. «Ishga tushir» dan keyin brauzer oynasining enini o'zgartirib ko'ring:

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

function useMeasure() {
  const [width, setWidth] = useState(0);
  const ref = useCallback((el) => {
    const ro = new ResizeObserver(([entry]) => {
      setWidth(Math.round(entry.contentRect.width));
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);
  return [ref, width];
}

export default function App() {
  const [ref, width] = useMeasure();
  const columns = width >= 480 ? 3 : width >= 320 ? 2 : 1;
  return (
    <div ref={ref}>
      <p>Eni: {width}px — ustunlar: {columns}</p>
    </div>
  );
}

Biz Chrome 155 da oyna enini uch marta o'zgartirib sinadik (600, 420 va 300 px; sahifa chetidagi 8 px lik bo'shliqlar ayiriladi):

text
Eni: 584px — ustunlar: 3
Eni: 404px — ustunlar: 2
Eni: 284px — ustunlar: 1

Ikki eslatma:

  • contentRect — padding siz ichki o'lcham. Chegara va padding bilan kerak bo'lsa — entry.borderBoxSize.
  • Har o'zgarishda state. Sudrash paytida ResizeObserver har kadrda xabar beradi va har safar komponent qayta chiziladi. Faqat ustunlar soni kerak bo'lsa, setColumns(hisob) — React bir xil qiymatda qayta chizmaydi (bailout).

Tayyor hook — react-use-measure (2.1.7): const [ref, bounds] = useMeasure() — bounds da width, height, top, left. Kanon uni o'lchamadi: vazifalar da o'lchamga bog'liq JavaScript yo'q — hammasi CSS bilan.

5.3 Telefonda: split view emas, ikki sahifa

Split view kompyuter ekrani uchun. 360 px lik telefonda ikki panel sig'maydi: 40% — 144 px, unda buyurtma raqami ham zo'rg'a ko'rinadi. Shuning uchun tor ekranda ro'yxat → tafsilot naqshi ishlatiladi (inglizcha master-detail): avval faqat ro'yxat, buyurtma bosilganda — faqat tafsilot va «Orqaga» tugmasi.

React'da buni ikki yo'l bilan qilish mumkin:

  • Router bilan (/buyurtmalar va /buyurtmalar/:id, 19-qism). Kompyuterda ikkala marshrut bitta layout ichida yonma-yon chiziladi (<Outlet /> o'ng panelda), telefonda esa — alohida sahifalar. Bu eng toza yo'l: "Orqaga" tugmasi va havolani ulashish o'zi ishlaydi.
  • Konteyner eni bilan — useMeasure dan kelgan en 640 px dan kichik bo'lsa, Group o'rniga bitta panel. Bu yo'l faqat ilovaning bir qismi (masalan, dashboard'dagi vidjet) uchun mos.

Ikkala holatda ham bitta qoida: kichik ekranda ajratkich chizilmaydi. Barmoq bilan 8 px lik chiziqni tortish noqulay va panelning o'zi sig'maydi. react-resizable-panels hujjati ham teginish uchun kattaroq nishonni tavsiya qiladi (resizeTargetMinimumSize, Apple tavsiyasiga ko'ra teginishda ~37 px).

Ichma-ich guruhlar ham ko'p uchraydi: chapda ro'yxat, o'ngda tepada tafsilot, pastda izohlar. Panel ichiga yana bir Group qo'yiladi, faqat yo'nalishi boshqa:

jsx
<Group orientation="horizontal">
  <Panel id="list" defaultSize="35">…</Panel>
  <Separator />
  <Panel id="right">
    <Group orientation="vertical">
      <Panel id="details">…</Panel>
      <Separator />
      <Panel id="comments" collapsible minSize="20">…</Panel>
    </Group>
  </Panel>
</Group>

Klaviatura bilan ishlaganda bir nechta ajratkich orasida Tab bilan yuriladi. APG naqshi yana bitta ixtiyoriy tugmani tilga oladi — F6: panellar orasida sakrash. Uni kod muharrirlari ishlatadi, oddiy veb-ilovada odatda shart emas.

5.4 Sticky

Split view'da yana bir CSS vositasi tez-tez kerak: panel ichida aylantirganda sarlavha tepada yopishib tursin. Bu position: sticky — JavaScript'siz (fixed va sticky darsi). Tuzog'i o'sha darsda: sticky o'zining eng yaqin aylantiriladigan ota-elementiga yopishadi. Split view'da har panel o'zi aylanadi (overflow: auto) — sarlavha aynan panelning tepasiga yopishadi, sahifaning emas. Bu aynan kerakli xulq.

6. Tajriba: nega vazifalar ga kirmadi

Bu darsda vazifalar kodi o'zgarmaydi. Kanon uchala vositani o'lchadi va hech birini olmadi.

Split view. vazifalar — kengligi 36rem (576 px) bitta ustunli ilova. Telefonda (360 px) ikkita panel umuman sig'maydi, kompyuterda esa ikkinchi panelga ko'rsatadigan narsa yo'q: vazifa tafsiloti 19-qismdan beri alohida sahifa (/vazifa/:id). +13.1 KB — hamma foydalanuvchi uchun, foydasi esa yo'q.

Masonry. Vazifalar — bir xil balandlikdagi qatorlar ro'yxati, kartalar to'ri emas. Masonry yechadigan muammo (har xil balandlikdagi kartalar) yo'q.

ResizeObserver. Ilovada o'lchamga qarab o'zgaradigan hamma narsa CSS bilan qilingan: media query'lar va Tailwind variantlari. JavaScript o'lchovi — qo'shimcha qayta renderlar.

Kanon qoidasi har darsda bir xil: kutubxona — muammo bor bo'lsa va narxi o'lchangan bo'lsa. Bu darsdagi vositalar admin panellar, pochta va dizayn dasturlari, galereyalar uchun — «Real ishda» bo'limida.

Bu darsdagi kutubxonalar narxi (JS gzip, asos — faqat React)
  • react-resizable-panels 4.14.3CSP 013,1 KB
  • masonic 4.1.0CSP 05,5 KB
  • o'z ajratkichimiz~60 qator0 KB

Manba: IlmHamroh kurs tajribasi — kutubxonalar o'lchovi (Vite 8.3.3, Chrome 155, 2026-10-14); o'z ajratkichimiz — shu darsdagi kod

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ajratkichni Tab bilan topib bo'lmaydi div fokus olmaydi role="separator" + tabIndex={0}
strelka bosilsa sahifa aylanadi preventDefault yo'q onKeyDown da e.preventDefault()
telefonda sudrash sahifani aylantiradi touch-action yo'q ajratkichda touch-action: none
sichqoncha ajratkichdan chiqsa, sudrash uziladi pointer capture yo'q setPointerCapture
sahifa yangilansa layout yo'qoladi saqlanmagan localStorage yoki useDefaultLayout
v4 da panel juda tor defaultSize={40} — piksel defaultSize="40" (foiz)
PanelGroup is not exported v3 kod, v4 paket Group, Panel, Separator
masonry'da tartib aralash CSS columns — ustun bo'yicha qator tartibi kerak bo'lsa — JS (masonic)
ResizeObserver loop completed with undelivered notifications kuzatuvchi ichida o'sha elementning o'lchamini o'zgartirish o'lchamni o'zgartirmang yoki keyingi kadrga qoldiring

Oxirgi qator — brauzer ogohlantirishi. Tarjimasi: "ResizeObserver sikli yetkazilmagan xabarlar bilan tugadi". Kuzatuvchi o'lcham haqida xabar berdi, siz unga javoban o'sha elementni kattalashtirdingiz, u yana xabar bermoqchi — brauzer cheksiz siklni bir kadrda to'xtatadi.

8. Mashqlar

Mashqlar kurs/mashqlar/20/18-layout-vositalari/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): ajratkichning ARIA'si

Ikki panel yonma-yon, orasida tik chiziq. Ajratkichga nima kerak?

  • Roli → role=
  • Chiziq yo'nalishi → aria-orientation=
  • Hozirgi o'lcham (foiz) →
  • Qaysi panelni o'zgartiradi →
Yechim
  • separator — va tabIndex={0}: fokus oladigan ajratkich.
  • vertical — chiziqning o'zi tik (panellar gorizontal joylashgan bo'lsa ham).
  • aria-valuenow — aria-valuemin/aria-valuemax bilan birga.
  • aria-controls — asosiy panelning id si.

2-mashq (o'rta): aria-valuetext va chegaralar

«O'z ajratkichingiz» misoliga qo'shing:

  1. aria-valuetext — Malika "40" emas, "Buyurtmalar 40 foiz" ni eshitsin.
  2. Home — 20%, End — 80% (misolda bor — sinab ko'ring).
  3. Ajratkich ostida role="status" emas, oddiy matn: «Buyurtmalar: 40% · Tafsilot: 60%».

Ishora: aria-valuetext — satr; aria-valuenow qoladi (ba'zi yordamchi texnologiyalar raqamni o'qiydi). Matn — shablon satr bilan.

Yechim
jsx
import { useState } from "react";

const MIN = 20;
const MAX = 80;
const clamp = (v) => Math.min(MAX, Math.max(MIN, v));

export default function App() {
  const [left, setLeft] = useState(40);
  function onKeyDown(e) {
    const keys = {
      ArrowLeft: left - 5,
      ArrowRight: left + 5,
      Home: MIN,
      End: MAX,
    };
    if (!(e.key in keys)) return;
    e.preventDefault();
    setLeft(clamp(keys[e.key]));
  }
  return (
    <div>
      <div style={{ display: "flex", height: 80 }}>
        <section id="orders" style={{ flexBasis: `${left}%` }} />
        <div
          role="separator"
          tabIndex={0}
          aria-label="Panel o'lchami"
          aria-controls="orders"
          aria-orientation="vertical"
          aria-valuenow={left}
          aria-valuemin={MIN}
          aria-valuemax={MAX}
          aria-valuetext={`Buyurtmalar ${left} foiz`}
          onKeyDown={onKeyDown}
          style={{ width: 8, background: "#99a" }}
        />
        <section style={{ flex: 1 }} />
      </div>
      <p>
        Buyurtmalar: {left}% · Tafsilot: {100 - left}%
      </p>
    </div>
  );
}

Ekranda:

text
Buyurtmalar: 75% · Tafsilot: 25%

3-mashq (qiyin): yig'iladigan panel

Ajratkichga Enter qo'shing: bosilganda chap panel yig'iladi (0%), yana bosilganda — yig'ilishdan oldingi o'lchamiga qaytadi. Konsolga har o'zgarish: chap: 40%. Yig'ilgan holatda strelkalar ishlamasin.

Ishora: oldingi o'lchamni eslab qolish — useRef (useRef I), chunki u ekranga chiqmaydi. aria-valuemin endi 0 (yig'ilgan holat ham qiymat).

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

const MIN = 20;
const MAX = 80;
const clamp = (v) => Math.min(MAX, Math.max(MIN, v));

export default function App() {
  const [left, setLeft] = useState(40);
  const beforeCollapse = useRef(40);
  useEffect(() => {
    console.log(`chap: ${left}%`);
  }, [left]);
  function onKeyDown(e) {
    if (e.key === "Enter") {
      e.preventDefault();
      if (left === 0) {
        setLeft(beforeCollapse.current);
      } else {
        beforeCollapse.current = left;
        setLeft(0);
      }
      return;
    }
    if (left === 0) return; // yig'ilganda — faqat Enter
    const keys = { ArrowLeft: left - 5, ArrowRight: left + 5 };
    if (!(e.key in keys)) return;
    e.preventDefault();
    setLeft(clamp(keys[e.key]));
  }
  return (
    <div style={{ display: "flex", height: 80 }}>
      <section id="orders" style={{ flexBasis: `${left}%` }} />
      <div
        role="separator"
        tabIndex={0}
        aria-label="Panel o'lchami"
        aria-controls="orders"
        aria-orientation="vertical"
        aria-valuenow={left}
        aria-valuemin={0}
        aria-valuemax={MAX}
        onKeyDown={onKeyDown}
        style={{ width: 8, background: "#99a" }}
      />
      <section style={{ flex: 1 }} />
    </div>
  );
}

Konsolda:

text
chap: 40%
chap: 45%
chap: 0%
chap: 45%

9. Real ishda

  • Admin panellar va ichki dasturlar — ro'yxat | tafsilot (CRM, pochta, buyurtmalar), kod muharrirlari, dashboard'lardagi o'zgaruvchan bloklar. shadcn'ning Resizable i — react-resizable-panels ustida.
  • Galereyalar va lentalar — Pinterest tipidagi rasmlar, mahsulot kartochkalari, portfolio. Kichik galereyaga CSS columns, cheksiz lentaga — JS masonry.
  • Moslashuvchan komponentlar — panel, karta yoki vidjet o'z eniga moslashadi: avval container queries (CSS), JavaScript o'lchov — faqat hisob kerak bo'lganda (masalan, masonry ustunlari).
  • Intervyu savollari: "Split view'ni a11y bilan qanday qilasiz?", "Masonry'ni CSS va JS bilan qilish farqi?", "ResizeObserver va media query farqi?", "Panel o'lchamini qayerda saqlaysiz?".

Xulosa

  • Ajratkich — role="separator", fokus, aria-valuenow/min/max, aria-controls, aria-orientation="vertical"; ← → Home End Enter; sichqonchada setPointerCapture, telefonda touch-action: none.
  • Layout — shaxsiy qulaylik: localStorage (tekshirib o'qing, dangasa boshlang'ich qiymat).
  • react-resizable-panels v4: Group/Panel/Separator, useDefaultLayout; son — piksel, satr — foiz. v3 (PanelGroup, PanelResizeHandle, autoSaveId) — eski kodda. +13.1 KB.
  • Masonry: CSS columns — ustun bo'yicha tartib, 0 KB; JS — eng qisqa ustunga, qator tartibi, masonic +5.5 KB (o'lchash + virtualizatsiya); grid-lanes — hali Baseline emas (Chrome 155 da yo'q).
  • ResizeObserver — element eni (panel, konteyner); avval container queries. sticky — panel ichida yopishadigan sarlavha.
  • vazifalar — 36rem bitta ustun: bu vositalar yechadigan muammo yo'q, shuning uchun kanonga kirmadi.

Keyingi dars: Node-diagramma va canvas: React Flow va Konva — vizual muharrirlar uchun tugun va bog'lanishlar (React Flow), canvas grafikasi (Konva) va qachon SVG, qachon canvas; Trusted Types ostida nima ochilmadi.

Manbalar

  • WAI-ARIA APG: Window Splitter Pattern — w3.org/WAI/ARIA/apg/patterns/windowsplitter
  • react-resizable-panels (4.14.3) — github.com/bvaughn/react-resizable-panels, react-resizable-panels.vercel.app
  • masonic (4.1.0) — github.com/jaredLunde/masonic
  • MDN: Resize Observer API, position: sticky, CSS multi-column layout — developer.mozilla.org
  • react-use-measure (2.1.7) — github.com/pmndrs/react-use-measure
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Resizable panellar, split view va masonry React'da: ajratkich, layoutni saqlash va ResizeObserver — IlmHamroh