Search

Ctrl + K

SortableList

This document outlines the steps to create SortableList component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • Node.js and npm installed on your machine.
  • Tailwind CSS installed in your project.
  • CVA(class-variance-authority) is a utility for managing CSS class names based on various conditions.
  • clsx is a tiny utility for constructing className strings conditionally.
Step 1: Create SortableList component
1// sortable-list.component.tsx
2import clsx from "clsx";
3import { GripIcon } from "lucide-react";
4import { useEffect, useRef } from "react";
5
6interface DataType {
7  id: string;
8  [key: string]: any;
9}
10
11export interface SortableListProps {
12  className?: string;
13  data: DataType[];
14  renderItem: (item: DataType) => React.ReactNode;
15  onChange?: (orderedIds: string[]) => void;
16}
17
18const SortableList: React.FC<SortableListProps> = ({
19  data,
20  className,
21  renderItem,
22  onChange,
23}) => {
24  const sortableListRef = useRef<HTMLUListElement>(null);
25  const orderedIds = useRef<string[]>(data.map((item) => item.id));
26
27  useEffect(() => {
28    const sortableList = sortableListRef.current!;
29    const items = sortableList.querySelectorAll("li");
30
31    const handleDragStart = (item: HTMLLIElement) => {
32      setTimeout(() => {
33        item.classList.add("dragging");
34        const child = item.querySelector("div")!;
35        child.style.opacity = "0";
36      }, 0);
37    };
38
39    const handleDragEnd = (item: HTMLLIElement) => {
40      item.classList.remove("dragging");
41      const child = item.querySelector("div")!;
42      child.style.opacity = "1";
43    };
44
45    items.forEach((item) => {
46      item.ondragstart = () => handleDragStart(item);
47      item.ondragend = () => handleDragEnd(item);
48    });
49
50    return () => {
51      items.forEach((item) => {
52        item.ondragstart = null;
53        item.ondragend = null;
54      });
55    };
56  }, []);
57
58  useEffect(() => {
59    const sortableList = sortableListRef.current!;
60
61    const initSortableList = (e: DragEvent) => {
62      e.preventDefault();
63
64      const listItems = Array.from(sortableList.querySelectorAll("li"));
65      const orderedList = listItems
66        .map((item) => {
67          const id = item.getAttribute("data-id") as string;
68          return id;
69        })
70        .filter((item) => item !== undefined);
71
72      const draggingItem = sortableList.querySelector(".dragging");
73
74      if (!draggingItem) return;
75
76      const siblings = Array.from(
77        sortableList.querySelectorAll("li:not(.dragging)"),
78      );
79
80      const nextSibling = siblings.find((sibling) => {
81        const rect = sibling.getBoundingClientRect();
82        return e.clientY <= rect.top + rect.height / 2;
83      });
84
85      sortableList.insertBefore(draggingItem, nextSibling || null);
86
87      if (orderedIds.current.join(",") !== orderedList.join(",")) {
88        orderedIds.current = orderedList;
89        onChange?.(orderedList);
90      }
91    };
92
93    const preventDragEnter = (e: DragEvent) => {
94      e.preventDefault();
95    };
96
97    sortableList.addEventListener("dragover", initSortableList);
98    sortableList.addEventListener("dragenter", preventDragEnter);
99
100    return () => {
101      sortableList.removeEventListener("dragover", initSortableList);
102      sortableList.removeEventListener("dragenter", preventDragEnter);
103    };
104
105    // eslint-disable-next-line react-hooks/exhaustive-deps
106  }, []);
107
108  return (
109    <ul
110      ref={sortableListRef}
111      className={clsx("grid grid-cols-1 gap-3", className)}
112    >
113      {data.map((item) => (
114        <li
115          key={item.id}
116          data-id={item.id}
117          draggable
118          className="flex cursor-move list-none items-center justify-between rounded-md border border-neutral-300 bg-white px-3 py-2 dark:border-neutral-600 dark:bg-neutral-600"
119        >
120          <div className="flex w-full items-center">
121            <div className="mr-2 flex-auto">{renderItem(item)}</div>
122            <GripIcon width={16} height={16} />
123          </div>
124        </li>
125      ))}
126    </ul>
127  );
128};
129
130export default SortableList;
131