Mundarija (38)
- Bu darsda
- 1. Nega bu kerak?
- 2. dnd-kit qanday tuzilgan
- 2.1 Qismlar
- 2.2 Versiya haqida
- 3. Birinchi tartiblanadigan ro'yxat
- 3.1 Minimal misol
- 3.2 useSortable nima beradi
- 3.3 Nega tekshiruv muhitida ↓ ishlamaydi
- 4. Sensorlar, tutqich va o'zbekcha e'lonlar
- 4.1 To'liq misol
- 4.2 Sensorlar
- 4.3 Tutqich
- 4.4 E'lonlar va ko'rsatma
- 5. Qo'yilgandan keyin: tartibni yangilash
- 5.1 arrayMove
- 5.2 Collision: "qaysi qator ostida?"
- 5.3 DragOverlay va kanban
- 6. Narxi: o'lchov va «Tartiblash» rejimi
- 6.1 Birinchi variant va uning raqamlari
- 6.2 Qaror: alohida rejim
- 7. Muqobillar
- 8. Vazifalar qadami: «Tartiblash» rejimi (holat 17)
- 8.1 Nima o'zgaradi
- 8.2 kochir: qaysi tartib yoziladi
- 8.3 Tutqichli qator
- 8.4 Rejim, kontekst va lazy
- 8.5 TartiblashRejimi va turlar shimi
- 8.6 Tekshiruvlar
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi qism?
- 2-mashq (o'rta): "Yuqoriga" va "Pastga" — sudrashsiz muqobil
- 3-mashq (qiyin): guruhdan chiqmasin
- 4-mashq: Vazifalar qadami
- 11. Real ishda
- Xulosa
- Manbalar
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+SortableContextichida, har qatoruseSortablebilan; qo'yilgandaonDragEndda 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:vazifalarda 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.
onDragEndda tartibni to'g'ri yangilaysiz, DragOverlay va kanban g'oyasini bilasiz.- dnd-kit'ning INP narxini o'lchov bilan ko'rasiz va
vazifalarda «Tartiblash» rejimini quriladi. - HTML DnD, pragmatic-drag-and-drop va
@dnd-kit/reactbilan 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:
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:
start: Osh
cancel: Osh
jonli hudud: Dragging was cancelled. Draggable item Osh was dropped.Ekranda:
Osh
Manti
Lag'monKonsolning 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:
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:
e'lon: «Non olish» olindi: 1-o'rin, jami 3.
e'lon: «Non olish» 1-o'ringa qo'yildi.Ekranda:
Non olish
Sut olish
Darsni takrorlashTekshiruv 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:
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» tutqichidaEsc 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: nonefaqat 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— tutqichgaaria-describedbyorqali 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.undefinedqaytarsa — 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):
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:
(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:
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):
- 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):
// #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):
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
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.2Diff: 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:
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-guruhTo'rt holat:
- Pastga (1 → 4 ning joyiga): 1 to'rtning keyiniga tushdi —
arrayMovebilan bir xil qoida. - Tepaga (5 → 2): 5 ikkining oldiga.
- Filtrlangan ekran (ekranda faqat 2 va 5): to'liq massivda yashirin vazifalar (1, 3, 4) joyida qoldi.
arrayMoveni ekrandagi ro'yxatga qo'llasak, yashirinlar haqida hech narsa bilmasdi — shuning uchun o'z funksiyamiz. - 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):
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/modifierspaketi (restrictToVerticalAxis) bor; kanon bitta qator uchun yangi paket qo'shmadi.- Ikki ref bitta tugmada — dnd-kit'ning
setActivatorNodeRefva 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'datouch-action: none, faqat tutqichda.
8.4 Rejim, kontekst va lazy
App.tsx da rejim sharti va tugma (kanondan):
// 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; <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-describedbyuni tugmaga bog'laydi.tartiblashFaol = tartiblash && sudrashMumkin— shart buzilsa (qidiruvga yozildi), rejim o'z-o'zidan o'chadi. Alohida effekt bilansetTartiblash(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):
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):
// 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:
// 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):
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):
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):
feat: vazifalar tartibi — sudrab (dnd-kit), klaviatura bilan ham; serverga PUT
perf: «Tartiblash» rejimi — dnd-kit faqat rejimda yuklanadi; oddiy rejimda sensorlar yo'qIkkinchisi — o'lchovdan keyingi tuzatish. Tekshiruvlar (haqiqiy, holat 17):
npm test— node:testpass 203(+5kochir), VitestTests 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
#17bo'limi:
✅ 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 })—SortableContextichida.setActivatorNodeRef—attributesvalistenersham 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
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:
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
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:
2,3,1,4
3,1,2,4
other-group
same
not-found
1,2,3,4Birinchi 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-….jsyo'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; tartibnionDragEndda o'zingiz yangilaysiz (arrayMove). - Tutqich:
setActivatorNodeRef,attributes/listenersunga,touch-action: nonefaqat unda;PointerSensordistance: 4. - Klaviatura: Space — olish/qo'yish, ↑↓, Esc. E'lonlar va ko'rsatma — o'zbekcha,
onDragOvero'zi ustida — jim; jonli hudud —<main>da. - Collision: ro'yxatga
closestCenter, kanban'gaclosestCorners;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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!