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

Drag and drop React'da: dnd-kit bilan tartiblash — sensorlar, klaviatura, e'lonlar va narx

Qisqacha: dnd-kit — React uchun sudrab tashlash kutubxonasi: brauzerning HTML DnD API'si emas, o'z sensorlari (sichqoncha/barmoq, klaviatura) bilan ishlaydi, shuning uchun telefonda ham, faqat klaviatura bilan ham ishlaydi. Ro'yxat DndContext + SortableContext ichida, har qator useSortable bilan; qo'yilganda onDragEnd da tartibni o'zingiz yangilaysiz (arrayMove). Klaviatura: tutqichda Space — olish, ↑↓ — siljitish, Space — qo'yish, Esc — bekor. Ekran o'quvchi uchun e'lonlar (announcements) o'zbekcha yoziladi. Narxi bor: vazifalar da birinchi yuklashga +15.3 KB va katak bosish INP 32 → 72 ms — shuning uchun dnd-kit alohida «Tartiblash» rejimida, kerak bo'lganda yuklanadi.

Bu darsda

  • dnd-kit qismlarini (DndContext, sensorlar, useSortable, collision) va ular qanday bog'lanishini tushuntirasiz.
  • Tartiblanadigan ro'yxat yozasiz: tutqich, sichqoncha va klaviatura bilan sudrash.
  • Ekran o'quvchi e'lonlari va ko'rsatmasini o'zbekcha qilasiz.
  • onDragEnd da tartibni to'g'ri yangilaysiz, DragOverlay va kanban g'oyasini bilasiz.
  • dnd-kit'ning INP narxini o'lchov bilan ko'rasiz va vazifalar da «Tartiblash» rejimini quriladi.
  • HTML DnD, pragmatic-drag-and-drop va @dnd-kit/react bilan solishtirasiz.

Oldin bilishingiz kerak: Drag and Drop API, Sichqoncha, pointer va touch hodisalari, memo, useMemo, useCallback, lazy va code splitting, Fokus boshqaruvi.

1. Nega bu kerak?

vazifalar dagi ro'yxat hozir uch xil tartibda ko'rinadi: qo'shilgan tartibda, alifbo bo'yicha va yangilari oldin. Jasur aka to'rtinchisini so'radi: "Muhimlarini o'zim tepaga qo'yay — qo'l bilan". Buning eng tabiiy yo'li — qatorni sichqoncha yoki barmoq bilan ushlab, boshqa joyga sudrab tashlash (drag and drop).

Drag and Drop API darsida brauzerning o'z DnD API'sini ko'rganmiz va uning uch chegarasini aytganmiz: telefonda ishonchsiz, klaviaturada umuman ishlamaydi, ekran o'quvchi nima bo'layotganini bilmaydi. O'sha darsda Pointer Events bilan oddiy tartiblashni ham yozganmiz — 70 qator, animatsiyasiz va klaviaturasiz. "Haqiqiy loyihada kutubxona kerak" deb va'da bergandik. Bugun o'sha va'da: dnd-kit.

Hayotiy o'xshatish: kitob javonini tartiblash. Kitobni olasiz (olish), qo'lingizda ushlab javon bo'ylab yurasiz — qolgan kitoblar surilib joy beradi (siljitish), keyin qo'yasiz yoki fikringizdan qaytib joyiga qaytarasiz (qo'yish yoki bekor qilish). Ko'zi ojiz odamga esa yoningizdagi kishi har qadamni aytib turadi: "Kitob olindi, endi uchinchi joyda". dnd-kit'da bu to'rt bosqich ham, "aytib turuvchi" ham bor.

2. dnd-kit qanday tuzilgan

2.1 Qismlar

dnd-kit — bir nechta paket. Kanonda uchtasi, aniq versiyalar bilan:

Paket Versiya Nima beradi
@dnd-kit/core 6.3.1 DndContext, sensorlar, useDraggable, useDroppable, collision, DragOverlay
@dnd-kit/sortable 10.0.0 tartiblash uchun tayyor qatlam: SortableContext, useSortable, arrayMove
@dnd-kit/utilities 3.2.2 yordamchilar, masalan CSS.Translate.toString()

Ular qanday ishlashini bitta sudrash misolida ko'ramiz:

flowchart LR
  S["Sensor<br/>sichqoncha / klaviatura"] --> C["DndContext<br/>active, koordinata"]
  C --> X["Collision<br/>qaysi qator ostida?"]
  X --> O["over"]
  O --> E["onDragEnd(active, over)"]
  E --> ST["sizning state'ingiz<br/>arrayMove"]
  C --> A["E'lonlar<br/>jonli hudud"]

Nimaga qarang: dnd-kit sizning ma'lumotingizni o'zi o'zgartirmaydi. U faqat "nima olindi" (active) va "nima ustiga qo'yildi" (over) ni aytadi. Tartibni yangilash — onDragEnd da, sizning state'ingizda. Headless g'oyasi: xulq kutubxonada, ma'lumot va ko'rinish sizda.

2.2 Versiya haqida

@dnd-kit/core 6.3.1 — 2024-yil dekabrdagi reliz, shundan beri yangi barqaror versiya chiqmagan. Muallif yangi avlodni @dnd-kit/react nomi bilan ishlab chiqmoqda (hozir 0.5.1, ya'ni 1.0 dan oldingi — API hali o'zgarishi mumkin). Kanon barqaror 6-avlodni tanladi; yangi API'ni «Muqobillar» bo'limida ko'rasiz.

Eski turlar bitta tuzoq beradi: dnd-kit 6.3.1 turlari global JSX nomlar fazosiga tayanadi, React 19 ning turlari (@types/react 19) esa uni olib tashlagan. Natija — o'nlab TS2503 xatosi. Buni «Vazifalar qadami» bo'limida yechamiz.

3. Birinchi tartiblanadigan ro'yxat

3.1 Minimal misol

Uchta taom. Har qator — useSortable bilan, ro'yxat — SortableContext ichida, hammasi — DndContext ichida. Hozircha faqat klaviatura sensori. «Ishga tushir» dan keyin Tab bilan birinchi qatorga keling, Space bosing, keyin Esc:

jsx
import { useState } from "react";
import {
  DndContext,
  KeyboardSensor,
  closestCenter,
  useSensor,
  useSensors,
} from "@dnd-kit/core";
import {
  SortableContext,
  sortableKeyboardCoordinates,
  useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

function Dish({ id }) {
  const { attributes, listeners, setNodeRef, transform, transition } =
    useSortable({ id });
  const style = {
    transform: CSS.Translate.toString(transform),
    transition,
  };
  return (
    <li ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {id}
    </li>
  );
}

export default function App() {
  const [dishes] = useState(["Osh", "Manti", "Lag'mon"]);
  const sensors = useSensors(
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );
  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragStart={({ active }) => console.log("start:", active.id)}
      onDragCancel={({ active }) => {
        console.log("cancel:", active.id);
        // dnd-kit'ning jonli hududi — ko'rinmas, lekin DOM'da
        setTimeout(() => {
          const live = document.querySelector("[id^=DndLiveRegion]");
          console.log("jonli hudud:", live.textContent);
        }, 50);
      }}
    >
      <SortableContext items={dishes}>
        <ul>
          {dishes.map((d) => (
            <Dish key={d} id={d} />
          ))}
        </ul>
      </SortableContext>
    </DndContext>
  );
}

Konsolda:

text
start: Osh
cancel: Osh
jonli hudud: Dragging was cancelled. Draggable item Osh was dropped.

Ekranda:

text
Osh
Manti
Lag'mon

Konsolning oxirgi qatori — biz yozmagan matn. Bu dnd-kit'ning e'loni: "Sudrash bekor qilindi. Osh elementi tashlandi". U jonli hududda (live region) turadi: ko'zga ko'rinmaydi (1×1 px, kesilgan), shuning uchun ekranda yo'q. Lekin DOM'da bor va Malikaning ekran o'quvchisi uni o'qiydi. Biz uni setTimeout bilan DOM'dan o'qib, konsolga chiqardik. Hozircha inglizcha — keyingi bo'limda o'zbekchalashtiramiz.

3.2 useSortable nima beradi

useSortable({ id }) olti narsa qaytaradi, har biri o'z joyiga:

Qiymat Qayerga Nima qiladi
setNodeRef qator (li) dnd-kit qatorni o'lchaydi va suradi
attributes ushlanadigan element role="button", tabindex="0", aria-roledescription="sortable", aria-describedby (ko'rsatma)
listeners ushlanadigan element sensorlarning hodisalari (onPointerDown, onKeyDown)
transform style qator hozir qancha surilgan (x, y)
transition style qo'shni qatorlar joy berganda silliq o'tish
isDragging sinf yoki uslub shu qator hozir olinganmi

CSS.Translate.toString(transform) — { x: 0, y: 48 } obyektini "translate3d(0px, 48px, 0)" satriga aylantiradi. Qator DOM'da joyidan qimirlamaydi — faqat ko'rinishda suriladi. Haqiqiy tartib faqat qo'yilgandan keyin, sizning state'ingizda o'zgaradi. Uslub style xususiyati orqali beriladi (CSSOM) — CSP buni to'smaydi (Radix chuqur darsidagi farq).

3.3 Nega tekshiruv muhitida ↓ ishlamaydi

Klaviatura bilan siljitish uchun sortableKeyboardCoordinates qatorlarning ekrandagi o'rnini (getBoundingClientRect) o'lchaydi: "pastdagi eng yaqin qator qayerda?". Tekshiruv muhitida (jsdom) haqiqiy joylashuv yo'q — hamma qatorning o'lchami nol. Shuning uchun yuqoridagi misolda faqat olish va bekor qilishni ko'rsatdik: ular o'lchovga bog'liq emas.

Haqiqiy brauzerda esa hammasi ishlaydi. Keyingi bo'limdagi misolni biz Chrome 155 da haqiqiy klaviatura va sichqoncha bilan sinadik — natijalar o'sha yerda. Siz ham «Ishga tushir» da ↓ ni bosib ko'ring.

4. Sensorlar, tutqich va o'zbekcha e'lonlar

4.1 To'liq misol

Endi vazifalar dagi kabi: har qatorda tutqich (⋮⋮ belgili tugma) — sudrash faqat undan boshlanadi. Ikki sensor va o'zbekcha e'lonlar:

jsx
import { useState } from "react";
import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";

const css = `
  li { display: flex; align-items: center; gap: 8px; padding: 4px;
    list-style: none; border-bottom: 1px solid #ddd;
    background: #fff; }
  .handle { margin-left: auto; cursor: grab; touch-action: none;
    border: 0; background: none; padding: 8px; }
`;

function Task({ task }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    setActivatorNodeRef,
    transform,
    transition,
  } = useSortable({ id: task });
  const style = {
    transform: CSS.Translate.toString(transform),
    transition,
  };
  return (
    <li ref={setNodeRef} style={style}>
      {task}
      <button
        ref={setActivatorNodeRef}
        className="handle"
        {...attributes}
        {...listeners}
        aria-label={`Tartibni o'zgartirish: ${task}`}
      >
        <GripVertical size={20} />
      </button>
    </li>
  );
}

export default function App() {
  const [tasks, setTasks] = useState([
    "Non olish",
    "Sut olish",
    "Darsni takrorlash",
  ]);
  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: { distance: 4 },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );
  const place = (id) => tasks.indexOf(id) + 1;
  // e'lonni konsolga ham yozamiz (ko'rish uchun) va qaytaramiz
  const say = (text) => {
    console.log("e'lon:", text);
    return text;
  };
  const announcements = {
    onDragStart: ({ active }) =>
      say(
        `«${active.id}» olindi: ${place(active.id)}-o'rin, ` +
          `jami ${tasks.length}.`,
      ),
    onDragOver: ({ active, over }) =>
      over && over.id !== active.id
        ? say(`«${active.id}» — ${place(over.id)}-o'rin.`)
        : undefined,
    onDragEnd: ({ active, over }) =>
      say(
        over
          ? `«${active.id}» ${place(over.id)}-o'ringa qo'yildi.`
          : `«${active.id}» joyida qoldi.`,
      ),
    onDragCancel: ({ active }) =>
      say(`Bekor qilindi: «${active.id}» joyida qoldi.`),
  };
  function onDragEnd({ active, over }) {
    if (over && active.id !== over.id) {
      setTasks((t) =>
        arrayMove(t, t.indexOf(active.id), t.indexOf(over.id)),
      );
    }
  }
  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={onDragEnd}
      accessibility={{
        announcements,
        screenReaderInstructions: {
          draggable:
            "Olish uchun Space bosing. Strelkalar bilan siljiting, " +
            "Space — qo'yish, Esc — bekor qilish.",
        },
      }}
    >
      <style>{css}</style>
      <SortableContext
        items={tasks}
        strategy={verticalListSortingStrategy}
      >
        <ul>
          {tasks.map((t) => (
            <Task key={t} task={t} />
          ))}
        </ul>
      </SortableContext>
    </DndContext>
  );
}

Konsolda:

text
e'lon: «Non olish» olindi: 1-o'rin, jami 3.
e'lon: «Non olish» 1-o'ringa qo'yildi.

Ekranda:

text
Non olish
Sut olish
Darsni takrorlash

Tekshiruv muhitida: Tab — birinchi tutqich, Space — olindi, yana Space — o'sha joyga qo'yildi. E'lonlar endi o'zbekcha. say — kichik yordamchi: matnni konsolga yozadi va o'zini qaytaradi, dnd-kit uni jonli hududga qo'yadi. Loyihada say shart emas — u faqat e'lonlarni ko'z bilan ko'rish uchun.

Biz shu blokni Chrome 155 da (puppeteer, haqiqiy klaviatura) sinadik. Tab, Space, ↓, Space — har qadamdan keyingi e'lon va natija:

text
Space → «Non olish» olindi: 1-o'rin, jami 3.
↓     → «Non olish» — 2-o'rin.
Space → «Non olish» 2-o'ringa qo'yildi.
tartib: Sut olish, Non olish, Darsni takrorlash
fokus: «Tartibni o'zgartirish: Non olish» tutqichida

Esc bilan esa: Bekor qilindi: «Non olish» joyida qoldi. va tartib o'zgarmadi. Sichqoncha bilan (uchinchi tutqichni birinchisi ustiga tortib) — «Darsni takrorlash» 1-o'ringa qo'yildi. va u ro'yxat boshiga o'tdi.

4.2 Sensorlar

Sensor — foydalanuvchi harakatini "sudrash boshlandi, koordinata o'zgardi, tugadi" tiliga o'giruvchi qism. dnd-kit'da to'rttasi tayyor:

Sensor Nima tinglaydi Qachon
PointerSensor pointer hodisalari (sichqoncha, barmoq, qalam) ko'p hollarda yagona kerakli
MouseSensor + TouchSensor alohida sichqoncha va touch sichqoncha va barmoqqa har xil sozlama kerak bo'lsa
KeyboardSensor keydown har doim — Bobur uchun

activationConstraint — sudrash qachon boshlanishi:

  • { distance: 4 } — 4 px siljigandan keyin. Oddiy bosish sudrash emas: tutqichni bosish ham, uning yonidagi tugmalarni bosish ham avvalgidek ishlaydi.
  • { delay: 250, tolerance: 5 } — 250 ms ushlab turgandan keyin (bu vaqtda 5 px dan ko'p qimirlamasa). Telefonda butun qator ushlanadigan bo'lsa kerak: aks holda barmoq bilan ro'yxatni aylantirish ham sudrashga aylanadi.

Klaviatura sensori standart holatda Space va Enter bilan oladi va qo'yadi, Esc bilan bekor qiladi, strelkalar bilan siljitadi. sortableKeyboardCoordinates — strelka bosilganda "qo'shni qator joyi" ni hisoblaydi.

4.3 Tutqich

Ikkinchi misolda attributes va listeners butun qatorga emas, tutqich tugmasiga berildi. setActivatorNodeRef — "sudrash shu elementdan boshlanadi" degan ref. Nega tutqich?

  • Qator ichidagi katak, matn va «O'chirish» tugmasi avvalgidek bosiladi — sudrash ularga xalaqit bermaydi.
  • touch-action: none faqat tutqichda. U butun qatorda bo'lsa, telefonda ro'yxatni barmoq bilan aylantirib bo'lmaydi (Drag and Drop API darsidagi tuzoq).
  • Tutqich — haqiqiy <button>: Tab bilan keladi, nomi bor («Tartibni o'zgartirish: Non olish»), Malika uni tugma deb eshitadi.

4.4 E'lonlar va ko'rsatma

accessibility sozlamasida ikki narsa:

  • screenReaderInstructions.draggable — tutqichga aria-describedby orqali bog'langan ko'rsatma. Malika tutqichga kelganda "Tartibni o'zgartirish: Non olish, tugma. Olish uchun Space bosing…" ni eshitadi. Standart matn inglizcha ("To pick up a draggable item, press the space bar…").
  • announcements — to'rt hodisa uchun funksiyalar: onDragStart, onDragOver, onDragEnd, onDragCancel. Har biri satr qaytaradi va dnd-kit uni jonli hududga yozadi. undefined qaytarsa — e'lon yo'q.

onDragOver dagi over.id !== active.id shartiga e'tibor bering. U kanondan: olingan zahoti over — qatorning o'zi (u hali o'z joyida). Shart bo'lmasa, darhol "1-o'rin" e'loni chiqib, "olindi" e'lonini bosib ketardi — Malika faqat "1-o'rin" ni eshitardi. Bu xatoni vazifalar ning e2e testi topgan.

Tekshirib ko'ring: E'lonlarda nega taom nomi «…» ichida va o'rin raqami bilan? "Element olindi" yetmaydimi?

Javob

Malika ekranni ko'rmaydi. "Element olindi" — qaysi element? "2-o'rin" — nechtadan? «Non olish» olindi: 1-o'rin, jami 3 — uchala savolga javob beradi: nima, qayerda, qanchadan. Siljitganda faqat yangi o'rin yetadi — nomi esa takrorlanadi, chunki e'lonlar orasida u boshqa narsani o'qigan bo'lishi mumkin. dnd-kit'ning standart inglizcha e'lonlari ham shunday tuzilgan, faqat id bilan ("Draggable item Osh…").

5. Qo'yilgandan keyin: tartibni yangilash

5.1 arrayMove

onDragEnd active va over ni beradi. Tartibni yangilash — arrayMove(massiv, eski, yangi): elementni bir joydan olib, boshqasiga qo'yadi va yangi massiv qaytaradi (immutable yangilash):

jsx
import { arrayMove } from "@dnd-kit/sortable";

const tasks = ["Non olish", "Sut olish", "Darsni takrorlash"];
console.log(arrayMove(tasks, 0, 2));
console.log(arrayMove(tasks, 2, 0));
console.log(tasks);

Konsolda:

text
(3) ['Sut olish', 'Darsni takrorlash', 'Non olish']
(3) ['Darsni takrorlash', 'Non olish', 'Sut olish']
(3) ['Non olish', 'Sut olish', 'Darsni takrorlash']

Uchinchi qator — asl massiv o'zgarmagan. Pastga ko'chirsa, element over ning keyiniga, tepaga ko'chirsa — oldiga tushadi: over ning o'rnini egallaydi.

Serverga saqlash — xuddi boshqa o'zgarishlardek: avval ekranda (optimistik), keyin so'rov (optimistik yangilash). vazifalar butun yangi tartibni PUT bilan yuboradi.

5.2 Collision: "qaysi qator ostida?"

over ni collision detection algoritmi aniqlaydi: olingan qator boshqalarning qaysi biri bilan "to'qnashdi" (to'qnashuv g'oyasi o'yinlardan tanish):

Algoritm Qanday tanlaydi Qachon
closestCenter markazi eng yaqin tartiblanadigan ro'yxat (kanon)
closestCorners burchaklari eng yaqin kanban: ustunlar ichida ro'yxatlar
rectIntersection eng ko'p ustma-ust tushgan turli o'lchamli nishonlar
pointerWithin sichqoncha ostidagisi aniq nishon (fayl papkasi) — klaviaturada ishlamaydi

5.3 DragOverlay va kanban

DragOverlay — sudralayotgan elementning "nusxasi" portal orqali chiziladi, asl qator esa joyida qoladi (masalan, xira bo'lib). Kerak bo'lganda: element ro'yxatdan chiqib boshqa konteynerga o'tadi yoki overflow: hidden uni kesadi:

jsx
const [activeId, setActiveId] = useState(null);

<DndContext
  onDragStart={({ active }) => setActiveId(active.id)}
  onDragEnd={(e) => { setActiveId(null); onDragEnd(e); }}
>
  <SortableContext items={tasks}>{/* … */}</SortableContext>
  <DragOverlay>
    {activeId ? <TaskCard task={activeId} /> : null}
  </DragOverlay>
</DndContext>

Kanban (ustunlar: "Rejada", "Jarayonda", "Bajarildi") — har ustun alohida SortableContext. Element boshqa ustunga o'tganda buni onDragOver da ko'rasiz (over boshqa ustunda) va state'ni o'sha paytda ko'chirasiz — qo'yishni kutmasdan, shunda yangi ustun unga joy beradi. Collision — closestCorners. Bu murakkabroq naqsh; vazifalar da faol va bajarilgan vazifalar orasida ko'chirish ataylab yo'q (sababi — «Vazifalar qadami» bo'limida).

6. Narxi: o'lchov va «Tartiblash» rejimi

6.1 Birinchi variant va uning raqamlari

Kanon avval eng to'g'ridan-to'g'ri yo'lni qildi: DndContext va sensorlar har doim, har qatorda useSortable. Hamma test o'tdi. Keyin o'lchandi:

  • Birinchi yuklash: +15.3 KB gzip — dnd-kit hamma foydalanuvchiga, hatto hech qachon tartiblamaydiganlarga ham.
  • INP (katak bosish): 150 vazifali ro'yxatda vazifani belgilash 32 ms dan 72 ms ga (mediana) sekinlashdi, eng uzuni — 128 ms.

Nega katak bosish sekinlashdi? Har useSortable qator — kontekst iste'molchisi va o'z o'lchovlari. Bitta qator o'zgarsa, dnd-kit konteksti orqali ko'p qator qayta render bo'ladi (qayta render sabablari). Hech kim sudramayapti, lekin narx to'lanyapti.

6.2 Qaror: alohida rejim

Kod ko'rigidagi qaror: "+15 KB va INP 72 ms — qabul qilinmaydi". Yechim — «Tartiblash» rejimi:

  • Oddiy rejimda dnd-kit yo'q: na kodi, na sensorlari, na useSortable. Qatorlar 19-qismdagidek oddiy.
  • «Tartiblash» tugmasi bosilganda dnd-kit kerak bo'lganda yuklanadi (lazy) va qatorlarda tutqich paydo bo'ladi.

Natija (bir sessiyada, bir usulda o'lchandi — 150 vazifa, katak bosish, mediana):

Katak bosish INP, 150 vazifa (mediana)
  • v9 (dnd-kit yo'q)eng uzuni 3216 ms
  • v10 oddiy rejimeng uzuni 4024 ms
  • v10 «Tartiblash» rejimidaeng uzuni 8048 ms

Manba: IlmHamroh kurs tajribasi — vazifalar ilovasi (Chrome 155, Event Timing, 6 bosish × 3 sahifa, 2026-10-14)

Tuzatishdan oldingi variant (dnd-kit doim) — 72 ms, boshqa sessiyada o'lchangan (21-inp.txt; o'sha sessiyada v9 — 32 ms). Shuning uchun u grafikda alohida yozildi: turli sessiyalarning raqamlarini bir grafikka qo'yish noto'g'ri — mashina holati har xil.

Uch raqamni o'qiymiz: oddiy rejimda v9 ga yaqin (16 → 24 ms, farqning bir qismi — Radix menyu va Motion, boshqa darslar). Rejimda 48 ms — hali ham yaxshi chegara (200 ms) ichida, va bu narxni faqat tartiblayotgan foydalanuvchi to'laydi. Birinchi yuklash: +15.3 → +1.6 KB (faqat tugma va lazy "qobiq"), dnd-kit chunk'i — 15.5 KB, rejim yoqilganda keladi.

7. Muqobillar

Yo'l Kuchli tomoni Zaif tomoni
HTML DnD API (W-58) 0 KB, fayllarni tashlash telefon, klaviatura, a11y — yo'q
dnd-kit 6 (kanon) sensorlar, klaviatura, e'lonlar, sortable 2024-yildan yangilanmagan, React 19 turlari bilan shim
@dnd-kit/react 0.5.1 yangi avlod API 1.0 dan oldin — o'zgarishi mumkin
pragmatic-drag-and-drop 4.0.0 Atlassian (Jira, Trello); HTML DnD ustida, kichik bo'laklarga bo'lingan klaviatura va a11y — o'zingiz qo'shasiz

pragmatic-drag-and-drop (Apache-2.0) — boshqacha falsafa: brauzerning o'z DnD API'sidan foydalanadi (fayllar va boshqa oynalardan sudrash ham ishlaydi) va React'ga bog'lanmagan. Klaviatura uchun Atlassian "sudrash o'rniga menyu amallari" ni (Yuqoriga/Pastga ko'chirish) tavsiya qiladi.

Biz sinab ko'rdik. Kanon dnd-kit'ni alohida o'lchadi (tajriba/kitlar/src/k/dndkit.tsx, kanondan aynan, qatorlar o'qish uchun bo'lingan):

tsx
// #17 @dnd-kit/core 6.3.1 + sortable 10.0.0
function Q({ id }: { id: number }) {
  const { attributes, listeners, setNodeRef, transform, transition } =
    useSortable({ id });
  return (
    <li
      ref={setNodeRef}
      style={{
        transform: CSS.Translate.toString(transform),
        transition,
      }}
      {...attributes}
      {...listeners}
    >
      {id}
    </li>
  );
}
function D() {
  const [l, s] = useState([1, 2, 3]);
  const sens = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );
  return (
    <DndContext
      sensors={sens}
      collisionDetection={closestCenter}
      onDragEnd={({ active, over }) => {
        if (over)
          s((x) =>
            arrayMove(
              x,
              x.indexOf(Number(active.id)),
              x.indexOf(Number(over.id)),
            ),
          );
      }}
    >
      <SortableContext items={l}>
        <ul>{l.map((i) => <Q key={i} id={i} />)}</ul>
      </SortableContext>
    </DndContext>
  );
}

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

text
asos            JS   67719 (+   0.0 KB) CSS      0 | CSP 0 | xato: yo'q
dndkit          JS   82617 (+  14.5 KB) CSS      0 | CSP 0 | xato: yo'q

+14.5 KB gzip va CSP 0 — vazifalar dagi +15.3 KB ga yaqin (u yerda e'lonlar va ilova kodi ham bor). dnd-kit hamma uslubni style xususiyati orqali beradi — <style> tegi yo'q.

8. Vazifalar qadami: «Tartiblash» rejimi (holat 17)

8.1 Nima o'zgaradi

bash
git switch -c feat/tartiblash
npm i -E @dnd-kit/core@6.3.1 @dnd-kit/sortable@10.0.0 \
  @dnd-kit/utilities@3.2.2

Diff: 13 fayl, +520 −18. Asosiy fayllar:

Fayl Nima
tartib.ts kochir() — sof funksiya: yangi tartib (+5 node test)
komponentlar/SudraladiganQator.tsx useSortable, tutqich qator oxirida
komponentlar/TartiblashRejimi.tsx DndContext, sensorlar, o'zbekcha e'lonlar — alohida chunk
kontekst/sudrash.ts rejim yoqilganmi — kontekst
App.tsx «Tartiblash» tugmasi, shart, lazy
dnd-kit-jsx.d.ts React 19 turlari uchun shim (TS2503)

8.2 kochir: qaysi tartib yoziladi

vazifalar da tartiblash faqat "Qo'shilgan tartibda" saralashda ishlaydi: shu tartib — massivdagi tartibning o'zi. Lekin ekranda qator filtr bilan bo'lishi mumkin (faol vazifalar). Shuning uchun kochir ikki ro'yxat oladi: to'liq massiv va ekrandagi. Funksiya — kanondan aynan, faqat VazifaMalumoti turi shu yerda e'lon qilingan (kanonda — turlar.ts dan import). Ostida — kanon testlaridagi holatlar:

ts
type VazifaMalumoti = {
  id: number;
  matn: string;
  bajarildi: boolean;
};

export type TartibNatijasi =
  | { ok: true; vazifalar: readonly VazifaMalumoti[]; joy: number }
  | { ok: false; sabab: "bir-xil" | "topilmadi" | "boshqa-guruh" };

export function kochir(
  vazifalar: readonly VazifaMalumoti[],
  korinadigan: readonly VazifaMalumoti[],
  aId: number,
  bId: number,
): TartibNatijasi {
  if (aId === bId) {
    return { ok: false, sabab: "bir-xil" };
  }
  const a = vazifalar.find((v) => v.id === aId);
  const b = vazifalar.find((v) => v.id === bId);
  const ekranA = korinadigan.findIndex((v) => v.id === aId);
  const ekranB = korinadigan.findIndex((v) => v.id === bId);
  if (a === undefined || b === undefined || ekranA < 0 || ekranB < 0) {
    return { ok: false, sabab: "topilmadi" };
  }
  // Faol va bajarilgan — alohida guruhlar: "standart" bajarilganni
  // baribir pastga tushiradi, ko'chirish ko'rinmay qolardi
  if (a.bajarildi !== b.bajarildi) {
    return { ok: false, sabab: "boshqa-guruh" };
  }
  const pastga = ekranA < ekranB;
  const qolgan = vazifalar.filter((v) => v.id !== aId);
  const joyB = qolgan.findIndex((v) => v.id === bId);
  const yangi = [
    ...qolgan.slice(0, pastga ? joyB + 1 : joyB),
    a,
    ...qolgan.slice(pastga ? joyB + 1 : joyB),
  ];
  return { ok: true, vazifalar: yangi, joy: ekranB + 1 };
}

const v = (id: number, bajarildi = false) => ({
  id,
  matn: `V${id}`,
  bajarildi,
});
const ROYXAT = [v(1), v(2), v(3, true), v(4), v(5)];
const idlar = (n: TartibNatijasi) =>
  n.ok ? n.vazifalar.map((x) => x.id).join(",") : n.sabab;

console.log(idlar(kochir(ROYXAT, ROYXAT, 1, 4))); // 2,3,4,1,5
console.log(idlar(kochir(ROYXAT, ROYXAT, 5, 2))); // 1,5,2,3,4
console.log(idlar(kochir(ROYXAT, [v(2), v(5)], 5, 2))); // 1,5,2,3,4
console.log(idlar(kochir(ROYXAT, ROYXAT, 1, 3))); // boshqa-guruh

To'rt holat:

  1. Pastga (1 → 4 ning joyiga): 1 to'rtning keyiniga tushdi — arrayMove bilan bir xil qoida.
  2. Tepaga (5 → 2): 5 ikkining oldiga.
  3. Filtrlangan ekran (ekranda faqat 2 va 5): to'liq massivda yashirin vazifalar (1, 3, 4) joyida qoldi. arrayMove ni ekrandagi ro'yxatga qo'llasak, yashirinlar haqida hech narsa bilmasdi — shuning uchun o'z funksiyamiz.
  4. Boshqa guruh (faol 1 → bajarilgan 3): rad etiladi. "Qo'shilgan tartibda" saralash bajarilganlarni baribir pastga tushiradi — ko'chirish ko'rinmay qolardi. Foydalanuvchi e'lon eshitadi: "Faol va bajarilgan vazifalar alohida tartiblanadi."

Natija turi — discriminated union: ok: true bo'lsa yangi massiv, false bo'lsa sabab. kochir React'siz va DOM'siz — node:test bilan tekshiriladi (tekshiruv/tartib.test.js, 5 test, jami 198 → 203).

8.3 Tutqichli qator

SudraladiganQator.tsx (kanondan, asosiy qismi):

tsx
export function SudraladiganQator({ vazifa, children }: Props) {
  const {
    attributes,
    listeners,
    setNodeRef,
    setActivatorNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: vazifa.id });
  const { tugmaRef } = useQatorAmallari();
  const fokusRef = tugmaRef(vazifa.id, "sudra");
  return (
    <li
      ref={setNodeRef}
      data-id={vazifa.id}
      // Faqat tik o'q: x = 0 (qator chapga-o'ngga surilmasin)
      style={{
        transform: CSS.Translate.toString(
          transform === null ? null : { ...transform, x: 0 },
        ),
        transition,
      }}
      className={cn(
        vazifa.bajarildi && "bajarildi",
        // Olingan qator ustida — soya; boshqalari uning
        // tagidan o'tadi
        isDragging && "relative z-10 bg-yuza shadow-soya",
      )}
    >
      {children}
      <button
        type="button"
        // Ikki ref: dnd-kit (faollashtiruvchi) va fokus Map'i
        // (17/#34)
        ref={(element) => {
          setActivatorNodeRef(element);
          const tozala = fokusRef(element);
          // React 19: ref callback qaytargan funksiya — element
          // olib tashlanganda (null bilan chaqirilmaydi) —
          // ikkalasini tozalaymiz
          return () => {
            setActivatorNodeRef(null);
            tozala?.();
          };
        }}
        data-amal="sudra"
        {...attributes}
        {...listeners}
        aria-label={`Tartibni o'zgartirish: ${vazifa.matn}`}
        className="ml-auto inline-flex min-h-bosish min-w-bosish cursor-grab touch-none items-center justify-center border-0 bg-transparent text-ikkinchi"
      >
        <GripVertical size={20} />
      </button>
    </li>
  );
}

(Izohlar telefon uchun qatorlarga bo'lingan, kod — kanondan aynan; className qatori 70 dan uzun.) Bugungi g'oyalar bitta joyda:

  • { ...transform, x: 0 } — qator faqat tik suriladi. dnd-kit'da bunga @dnd-kit/modifiers paketi (restrictToVerticalAxis) bor; kanon bitta qator uchun yangi paket qo'shmadi.
  • Ikki ref bitta tugmada — dnd-kit'ning setActivatorNodeRef va 17-qismdagi fokus ref Map'i. React 19 ref callback cleanup'i ikkalasini tozalaydi.
  • Tutqich qator oxirida — Tab tartibi buzilmaydi: katak, matn, amallar, keyin tutqich.
  • touch-none — Tailwind'da touch-action: none, faqat tutqichda.

8.4 Rejim, kontekst va lazy

App.tsx da rejim sharti va tugma (kanondan):

tsx
  // Qachon: "standart" tartib (massiv tartibi — qo'lda tartib shu),
  // qidiruv bo'sh (bo'lmasa joy noaniq), tahrir yopiq, 2–200 qator
  // (200 dan ko'p — virtual ro'yxat, 18/#23: ko'rinmaydigan
  // qatorlar DOM'da yo'q)
  const sudrashMumkin =
    saralash === "standart" &&
    normalla(sorov) === "" &&
    tahrirId === null &&
    korinadigan.length > 1 &&
    korinadigan.length <= 200;
  const [tartiblash, setTartiblash] = useState(false);
  const tartiblashFaol = tartiblash && sudrashMumkin;
tsx
        <Tugma
          id="tartiblash-tugma"
          aria-pressed={tartiblashFaol}
          aria-describedby={
            sudrashMumkin ? undefined : tartiblashIzohId
          }
          disabled={!sudrashMumkin}
          variant={tartiblashFaol ? "asosiy" : "oddiy"}
          className="inline-flex items-center gap-2"
          onPointerEnter={tartiblashniOldindanYukla}
          onClick={() => {
            startTransition(() => setTartiblash(!tartiblashFaol));
          }}
        >
          <ArrowUpDown size={16} />
          Tartiblash
        </Tugma>

(Kanonda izohlar bitta qatorda, bu yerda telefon uchun bo'lingan.) O'qiymiz:

  • aria-pressed — tugma "bosilgan/bosilmagan" holatli (toggle). Malika «Tartiblash, almashtirish tugmasi, bosilgan» ni eshitadi.
  • disabled + sabab — shart bajarilmasa (masalan, alifbo tartibi), tugma o'chiq va yonida izoh: «Qo'shilgan tartibda», qidiruvsiz, 2–200 vazifa bo'lganda. aria-describedby uni tugmaga bog'laydi.
  • tartiblashFaol = tartiblash && sudrashMumkin — shart buzilsa (qidiruvga yozildi), rejim o'z-o'zidan o'chadi. Alohida effekt bilan setTartiblash(false) qilish shart emas — qiymat hisoblanadi.
  • 200 qator chegarasi — undan ko'pida ro'yxat virtual: ko'rinmaydigan qatorlar DOM'da yo'q, dnd-kit ularni o'lchay olmaydi (TEXNIK-QARZ 55).
  • startTransition — rejimga kirish transition: dnd-kit chunk'i kelguncha eski ro'yxat ko'rinib turadi, Suspense fallback'iga almashmaydi.
  • onPointerEnter — sichqoncha tugmaga kelsa, chunk oldindan yuklanadi (buyruqlar oynasi dagi naqsh).

lazy va modul Promise'i (statistika paneli va buyruqlar oynasidagi naqsh):

tsx
let tartiblashModuli: Promise<
  typeof import("./komponentlar/TartiblashRejimi.tsx")
> | null = null;
function tartiblashModuliniOl() {
  tartiblashModuli ??= import("./komponentlar/TartiblashRejimi.tsx");
  return tartiblashModuli;
}
const TartiblashRejimi = lazy(tartiblashModuliniOl);

Eng nozik joy — oddiy qator dnd-kit'ni import qilmasligi kerak. Agar OddiyQator.tsx SudraladiganQator ni import qilsa, Vite dnd-kit'ni asosiy chunk'ka qo'shadi va hamma ish behuda. Kanon yechimi — kontekst (kontekst/sudrash.ts):

tsx
// Komponent emas, chizuvchi FUNKSIYA: kontekstdan olingan komponentni
// <X /> qilib chizish React Compiler lint'ida xato
// (static-components — "render paytida yaratilgan komponent",
// o'lchandi)
export type TutqichliQator = (
  props: ComponentProps<typeof SudraladiganQator>,
) => ReactNode;

export const SudrashKonteksti = createContext<TutqichliQator | null>(
  null,
);

Oddiy rejimda kontekst qiymati null — OddiyQator odatdagi <Qator> ni chizadi. Rejimda TartiblashRejimi (lazy chunk) kontekstga funksiya beradi: (props) => <SudraladiganQator {...props} />. OddiyQator uni chaqiradi: tutqichliQator({ vazifa, children: tarkib }). import type — faqat tur, kod emas: build'da yo'qoladi.

8.5 TartiblashRejimi va turlar shimi

TartiblashRejimi.tsx — darsdagi to'liq misolning vazifalar dagi nusxasi: PointerSensor (distance: 4), KeyboardSensor, closestCenter, o'zbekcha e'lonlar (onDragOver da over.id === active.id — undefined) va ko'rsatma. Bitta farq — jonli hudud qayerda:

tsx
  // E'lonlar va ko'rsatma (aria-describedby) — <main> ichida
  // (portal)
  const portal = portalJoyi();
  // … accessibility={{ announcements, screenReaderInstructions,
  //   ...(portal === null ? {} : { container: portal }) }}

dnd-kit jonli hudud va ko'rsatmani sukut bo'yicha DndContext turgan joyning o'zida chizadi (portalsiz — biz dnd-kit kodida tekshirdik: container berilmasa, createPortal yo'q). vazifalar da esa TartiblashRejimi <ul> ichida turadi — <ul> ning bolasi faqat <li> bo'lishi mumkin, <div> emas. Shuning uchun container beriladi — Radix menyusidagi kabi <main> (portalJoyi()): landmark ichida va ro'yxatdan tashqarida.

Turlar shimi — src/dnd-kit-jsx.d.ts (kanondan):

ts
import type { JSX as ReactJSX } from "react";

declare global {
  namespace JSX {
    type Element = ReactJSX.Element;
    type IntrinsicElements = ReactJSX.IntrinsicElements;
  }
}

Shimsiz tsc -b (natijalar/17-dnd-kit-jsx.txt, birinchi qatorlari):

text
node_modules/@dnd-kit/core/dist/components/Accessibility/Accessibility.d.ts(9,129): error TS2503: Cannot find namespace 'JSX'.
node_modules/@dnd-kit/core/dist/components/DragOverlay/DragOverlay.d.ts(8,28): error TS2503: Cannot find namespace 'JSX'.

Tarjimasi: "JSX nomlar fazosi topilmadi". dnd-kit turlari 2024-yilda global JSX bilan yozilgan, React 19 turlari esa faqat React.JSX ni beradi. Ikki yo'l bor edi: skipLibCheck: true — hamma kutubxonalar turlarini tekshirmaslik, yoki 6 qatorlik shim — global JSX ni React.JSX ga bog'lash (module augmentation). Kanon shimni tanladi va uni TEXNIK-QARZ 54 ga yozdi: dnd-kit yangilanganda olib tashlanadi.

8.6 Tekshiruvlar

Commit'lar (kanon, ikkitasi):

text
feat: vazifalar tartibi — sudrab (dnd-kit), klaviatura bilan ham; serverga PUT
perf: «Tartiblash» rejimi — dnd-kit faqat rejimda yuklanadi; oddiy rejimda sensorlar yo'q

Ikkinchisi — o'lchovdan keyingi tuzatish. Tekshiruvlar (haqiqiy, holat 17):

  • npm test — node:test pass 203 (+5 kochir), Vitest Tests 65 passed (65); lint, format:check, tip, knip — toza. axe — 8 ko'rinishda 0 (yangi ko'rinish: «Tartiblash» rejimi, tutqichlar bilan).
  • Build (tekshiruv-17.txt): TartiblashRejimi-….js — 15.50 KB gzip, alohida chunk; birinchi yuklash +1.6 KB.
  • Brauzer (Chrome 155): 159, 72, 100 va 20-qism ssenariysidagi #17 bo'limi:
text
✅ boshida (standart): faollar oldin: [1,3,2]
✅ oddiy rejim: dnd-kit yuklanmagan, tutqich yo'q, tugma aria-pressed=false: [false,0,["false",false,0]]
✅ «Tartiblash» (Enter): chunk yuklandi, tutqichlar, aria-pressed=true, fokus tugmada: [true,["true",false,3],"tartiblash-tugma"]
✅ tutqich: har qatorda, oxirida, nomi va ko'rsatma (aria-describedby): [3,"sortable","Tartibni o'zgartirish: Non olish #bozor",true,true]
✅ Space — olindi (e'lon): "«Non olish #bozor» olindi: 1-o'rin, jami 3."
✅ ↓, Space — qo'yildi: tartib, e'lon, fokus tutqichda: [[3,1,2],true,["sudra","1"]]
✅ serverga PUT — butun ro'yxat yangi tartibda: [1,[2,3,1]]
✅ Esc — bekor qilindi, tartib o'zgarmadi, PUT yo'q: [[3,1,2],1,true]
✅ faol → bajarilganlar orasiga — yo'q, e'lon: [[3,1,2],"Faol va bajarilgan vazifalar alohida tartiblanadi."]
✅ sichqoncha bilan sudrab — tartib, PUT: [[1,3,2],2]
✅ yangilangandan keyin — tartib saqlangan: [1,3,2]
✅ «Alifbo» tartibida — tutqich yo'q: 0
✅ «Alifbo»: tugma o'chirilgan (disabled) va sababi (aria-describedby): [true,true]
✅ yangilangandan keyin — rejim o'chiq (state), tugma yana faol: ["false",false,0]
✅ rejimdan chiqish (tugma qayta bosildi): tutqich yo'q, aria-pressed=false: ["false",false,0]
✅ sudrash: CSP buzilishi yo'q: []
✅ konsolda xato yo'q: []

Ikkinchi qator — eng muhim: oddiy rejimda dnd-kit chunk'i umuman yuklanmagan. Oltinchi qator — Bobur yo'li to'liq: Space, ↓, Space, tartib [1,3,2] dan [3,1,2] ga, fokus tutqichda qoldi. "Yangilangandan keyin — rejim o'chiq": rejim — oddiy state, URL'da emas; sahifa yangilansa, foydalanuvchi oddiy rejimda qaytadi (bu ataylab).

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
qatorlar qimirlaydi, lekin tartib o'zgarmaydi onDragEnd da state yangilanmaydi arrayMove bilan yangi massiv
telefonda ro'yxat aylanmaydi touch-action: none butun qatorda faqat tutqichda
tugma bosilsa sudrash boshlanadi PointerSensor cheklovsiz activationConstraint: { distance: 4 }
Bobur sudray olmaydi faqat PointerSensor KeyboardSensor + sortableKeyboardCoordinates
ekran o'quvchi inglizcha gapiradi standart e'lonlar accessibility.announcements va ko'rsatma
"olindi" e'loni eshitilmaydi onDragOver o'zi ustida ham e'lon qiladi over.id === active.id → undefined
filtrlangan ro'yxatda boshqa vazifalar "sakraydi" arrayMove ekrandagi ro'yxatga to'liq massiv bilan ishlaydigan funksiya (kochir)
Cannot find namespace 'JSX' (TS2503) dnd-kit 6 turlari + React 19 JSX shimi (yoki skipLibCheck, kamroq yaxshi)
ro'yxat sekinlashdi (INP) DndContext/useSortable doim alohida rejim, lazy
virtual ro'yxatda sudrab bo'lmaydi ko'rinmaydigan qatorlar DOM'da yo'q chegaralash yoki "Yuqoriga/Pastga" amallari

10. Mashqlar

Mashqlar kurs/mashqlar/20/17-dnd-kit/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi qism?

  • Ro'yxat qatoriga sudrash xulqini beradigan hook →
  • Sudrash faqat tutqichdan boshlansin — tutqichga beriladigan ref →
  • Klaviatura bilan qo'shni qatorga siljitish koordinatasi →
  • Massivda elementni boshqa joyga ko'chiradigan yordamchi →
Yechim
  • useSortable({ id }) — SortableContext ichida.
  • setActivatorNodeRef — attributes va listeners ham shu elementga.
  • sortableKeyboardCoordinates — useSensor(KeyboardSensor, { coordinateGetter }).
  • arrayMove(massiv, eski, yangi) — yangi massiv, asl o'zgarmaydi.

2-mashq (o'rta): "Yuqoriga" va "Pastga" — sudrashsiz muqobil

Sudrash har doim ham mumkin emas (virtual ro'yxat, eski brauzer, harakat qiyin bo'lgan foydalanuvchi). Har qatorga ikki tugma qo'shing: «↑» va «↓». Tugma nomi (aria-label) — «Non olish — yuqoriga». Birinchi qatorda «↑», oxirgisida «↓» — disabled. Ostida role="status": «Non olish: 2-o'rin».

Ishora: arrayMove dnd-kit'siz ham ishlatiladi (u faqat massiv funksiyasi). Holat yangilanganda e'lonni ham yangilang — bitta useState matn uchun.

Yechim
jsx
import { useState } from "react";
import { arrayMove } from "@dnd-kit/sortable";

export default function App() {
  const [tasks, setTasks] = useState([
    "Non olish",
    "Sut olish",
    "Darsni takrorlash",
  ]);
  const [message, setMessage] = useState("");
  function move(task, step) {
    const from = tasks.indexOf(task);
    const to = from + step;
    setTasks(arrayMove(tasks, from, to));
    setMessage(`${task}: ${to + 1}-o'rin`);
  }
  return (
    <div>
      <ul>
        {tasks.map((task, i) => (
          <li key={task}>
            {task}{" "}
            <button
              aria-label={`${task} — yuqoriga`}
              disabled={i === 0}
              onClick={() => move(task, -1)}
            >
              ↑
            </button>
            <button
              aria-label={`${task} — pastga`}
              disabled={i === tasks.length - 1}
              onClick={() => move(task, 1)}
            >
              ↓
            </button>
          </li>
        ))}
      </ul>
      <p role="status">{message}</p>
    </div>
  );
}

Ekranda:

text
Sut olish ↑↓
Darsni takrorlash ↑↓
Non olish ↑↓
Darsni takrorlash: 2-o'rin

«Non olish» pastga — ikkinchi o'ringa, keyin «Darsni takrorlash» yuqoriga — u ham ikkinchiga, «Non olish» esa oxiriga surildi. Ekranda tugmalar belgisi (↑↓) ko'rinadi, Malika esa ularni aria-label bilan eshitadi: «Non olish — pastga, tugma». Bu yo'l klaviatura, ekran o'quvchi va qo'li titraydigan foydalanuvchi uchun sudrashdan ham qulayroq — shuning uchun ko'p ilovalarda ikkalasi birga bo'ladi.

3-mashq (qiyin): guruhdan chiqmasin

kochir kabi sof funksiya yozing, lekin soddaroq: moveInGroup(tasks, fromId, toId) — elementni toId ning o'rniga ko'chiradi, faqat ikkalasi bir guruhda bo'lsa (done maydoni bir xil). Natija: { ok: true, tasks } yoki { ok: false, reason } — sabablar "same", "not-found", "other-group". Asl massiv o'zgarmasin.

Ishora: «kochir: qaysi tartib yoziladi» bo'limi. Ko'chirishning o'zi — arrayMove (@dnd-kit/sortable dan) yoki slice bilan. Natija shakli — discriminated union.

Yechim
jsx
import { arrayMove } from "@dnd-kit/sortable";

function moveInGroup(tasks, fromId, toId) {
  if (fromId === toId) return { ok: false, reason: "same" };
  const from = tasks.findIndex((t) => t.id === fromId);
  const to = tasks.findIndex((t) => t.id === toId);
  if (from < 0 || to < 0) return { ok: false, reason: "not-found" };
  if (tasks[from].done !== tasks[to].done) {
    return { ok: false, reason: "other-group" };
  }
  return { ok: true, tasks: arrayMove(tasks, from, to) };
}

const TASKS = [
  { id: 1, done: false },
  { id: 2, done: false },
  { id: 3, done: false },
  { id: 4, done: true },
];
const ids = (r) =>
  r.ok ? r.tasks.map((t) => t.id).join(",") : r.reason;

console.log(ids(moveInGroup(TASKS, 1, 3)));
console.log(ids(moveInGroup(TASKS, 3, 1)));
console.log(ids(moveInGroup(TASKS, 2, 4)));
console.log(ids(moveInGroup(TASKS, 2, 2)));
console.log(ids(moveInGroup(TASKS, 9, 1)));
console.log(TASKS.map((t) => t.id).join(","));

Konsolda:

text
2,3,1,4
3,1,2,4
other-group
same
not-found
1,2,3,4

Birinchi ikki qator — guruh ichida ko'chirish (arrayMove qoidasi: 1 uchning o'rniga — oxiriga, 3 birning o'rniga — boshiga). Keyin uch rad etish va oxirgi qator — asl massiv o'zgarmagan. Bu funksiya React'siz: uni onDragEnd da ham, «↑/↓» tugmalarida ham, testda ham bir xil ishlatasiz.

4-mashq: Vazifalar qadami

feat/tartiblash branch'ida: paketlar (-E), tartib.ts + testlar, SudraladiganQator.tsx, TartiblashRejimi.tsx (default eksport — lazy uchun), kontekst/sudrash.ts, OddiyQator.tsx (kontekstdan chizuvchi funksiya), App.tsx (shart, tugma, lazy, Suspense), dnd-kit-jsx.d.ts va tsconfig.test.json ga shu fayl. Avval rejimsiz variantni qilib, INP'ni o'lchab ko'rsangiz — «Narxi: o'lchov va «Tartiblash» rejimi» bo'limidagi farqni o'zingiz ko'rasiz.

Yechim

Tekshiruv ro'yxati:

  • DevTools → Network: sahifa ochilganda TartiblashRejimi-….js yo'q. «Tartiblash» ni bosing — paydo bo'ladi (yoki sichqoncha tugmaga kelganda).
  • Faqat klaviatura: Tab bilan tutqichga, Space, ↓, Space — tartib o'zgardi, fokus tutqichda. Esc — bekor.
  • Ekran o'quvchi (NVDA yoki VoiceOver) bilan: «… olindi: 1-o'rin, jami N» eshitiladi.
  • «Alifbo bo'yicha» ni tanlang — tugma o'chiq, yonida sabab. Qidiruvga yozing — rejim o'zi o'chadi.
  • npm test — node 203, Vitest 65; npm run tip — 0 xato (shimsiz — 14 ta TS2503).

11. Real ishda

  • Vazifa va loyiha boshqaruvi — Trello, Jira, Notion: kanban, ro'yxatlar, ustunlar tartibi. Atlassian o'z mahsulotlari uchun pragmatic-drag-and-drop'ni yaratgan.
  • Admin panellar — menyu bandlari, kategoriyalar, banner'lar tartibi, forma quruvchilar (maydonlarni sudrab joylash).
  • Performans madaniyati — "kutubxona qo'shildi, test o'tdi" yetmaydi: INP va birinchi yuklashni o'lchab, kerak bo'lsa kechiktirib yuklash. Bu dars — shuning real misoli.
  • Intervyu savollari: "HTML DnD va dnd-kit farqi?", "Sudrashni klaviatura bilan qanday qilasiz?", "Ekran o'quvchi sudrashni qanday biladi?", "Kanban'da ustunlar orasida ko'chirish qanday?", "Kutubxona ilovani sekinlashtirsa nima qilasiz?".

Xulosa

  • dnd-kit — sensorlar (pointer, klaviatura) asosida, HTML DnD'siz: telefon va klaviatura ishlaydi. DndContext → SortableContext → useSortable; tartibni onDragEnd da o'zingiz yangilaysiz (arrayMove).
  • Tutqich: setActivatorNodeRef, attributes/listeners unga, touch-action: none faqat unda; PointerSensor distance: 4.
  • Klaviatura: Space — olish/qo'yish, ↑↓, Esc. E'lonlar va ko'rsatma — o'zbekcha, onDragOver o'zi ustida — jim; jonli hudud — <main> da.
  • Collision: ro'yxatga closestCenter, kanban'ga closestCorners; DragOverlay — konteynerdan chiqadigan nusxa.
  • Narx o'lchanadi: dnd-kit doim — +15.3 KB va INP 32 → 72 ms; «Tartiblash» rejimi (lazy) — +1.6 KB, oddiy rejimda 24 ms.
  • Muqobillar: HTML DnD (fayllar uchun), pragmatic-drag-and-drop, @dnd-kit/react (1.0 dan oldin); sudrashsiz yo'l — «↑/↓» tugmalari.

Keyingi dars: Layout vositalari: resizable panellar, split view, masonry — o'lchami o'zgaradigan panellar (react-resizable-panels), klaviatura bilan ajratkich, CSS masonry va ResizeObserver; nega ular vazifalar ga kirmadi.

Manbalar

  • dnd-kit: Docs — Sortable, Sensors, Accessibility, Collision detection, DragOverlay — docs.dndkit.com (@dnd-kit/core 6.3.1, sortable 10.0.0)
  • Atlassian: Pragmatic drag and drop — atlassian.design/components/pragmatic-drag-and-drop (4.0.0)
  • WAI-ARIA: aria-pressed, aria-roledescription, live regions — w3.org/TR/wai-aria-1.2
  • web.dev: Interaction to Next Paint (INP) — web.dev/articles/inp
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Drag and drop React'da: dnd-kit bilan tartiblash — sensorlar, klaviatura, e'lonlar va narx — IlmHamroh