1import React from "react"
2import type {
3 DndContextProps,
4 UniqueIdentifier,
5} from "@dnd-kit/core";
6import {
7 closestCenter,
8 DndContext,
9 KeyboardSensor,
10 MouseSensor,
11 TouchSensor,
12 useSensor,
13 useSensors,
14} from "@dnd-kit/core";
15import {
16 restrictToParentElement,
17 restrictToVerticalAxis,
18} from "@dnd-kit/modifiers";
19import {
20 arrayMove,
21 SortableContext,
22 verticalListSortingStrategy,
23 type SortableContextProps,
24} from "@dnd-kit/sortable";
25import { SortableDragHandle } from "./drag-handle";
26import { SortableItem } from "./item";
27import { SortableOverlay } from "./overlay";
28
29interface SortableProps<TData extends { id: UniqueIdentifier }>
30 extends DndContextProps {
31 /**
32 * An array of data items that the sortable component will render.
33 * @example
34 * value={[
35 * { id: 1, name: 'Item 1' },
36 * { id: 2, name: 'Item 2' },
37 * ]}
38 */
39 value: TData[]
40
41 /**
42 * An optional callback function that is called when the order of the data items changes.
43 * It receives the new array of items as its argument.
44 * @example
45 * onValueChange={(items) => console.log(items)}
46 */
47 onValueChange?: (items: TData[]) => void
48
49 /**
50 * An optional callback function that is called when an item is moved.
51 * It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item.
52 * This will override the default behavior of updating the order of the data items.
53 * @type (event: { activeIndex: number; overIndex: number }) => void
54 * @example
55 * onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)}
56 */
57 onMove?: (event: { activeIndex: number; overIndex: number }) => void
58
59 /**
60 * A collision detection strategy that will be used to determine the closest sortable item.
61 * @default closestCenter
62 * @type DndContextProps["collisionDetection"]
63 */
64 collisionDetection?: DndContextProps["collisionDetection"]
65
66 /**
67 * An array of modifiers that will be used to modify the behavior of the sortable component.
68 * @default
69 * [restrictToVerticalAxis, restrictToParentElement]
70 * @type Modifier[]
71 */
72 modifiers?: DndContextProps["modifiers"]
73
74 /**
75 * A sorting strategy that will be used to determine the new order of the data items.
76 * @default verticalListSortingStrategy
77 * @type SortableContextProps["strategy"]
78 */
79 strategy?: SortableContextProps["strategy"]
80
81 /**
82 * An optional React node that is rendered on top of the sortable component.
83 * It can be used to display additional information or controls.
84 * @default null
85 * @type React.ReactNode | null
86 * @example
87 * overlay={<Skeleton className="w-full h-8" />}
88 */
89 overlay?: React.ReactNode | null
90}
91
92function Sortable<TData extends { id: UniqueIdentifier }>({
93 value,
94 onValueChange,
95 collisionDetection = closestCenter,
96 modifiers = [restrictToVerticalAxis, restrictToParentElement],
97 strategy = verticalListSortingStrategy,
98 onMove,
99 children,
100 overlay,
101 ...props
102}: SortableProps<TData>) {
103 const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null)
104
105 const sensors = useSensors(
106 useSensor(MouseSensor),
107 useSensor(TouchSensor),
108 useSensor(KeyboardSensor)
109 )
110
111 return (
112 <DndContext
113 modifiers={modifiers}
114 sensors={sensors}
115 onDragStart={({ active }) => setActiveId(active.id)}
116 onDragEnd={({ active, over }) => {
117 if (over && active.id !== over?.id) {
118 const activeIndex = value.findIndex((item) => item.id === active.id)
119 const overIndex = value.findIndex((item) => item.id === over.id)
120
121 if (onMove) {
122 onMove({ activeIndex, overIndex })
123 } else {
124 onValueChange?.(arrayMove(value, activeIndex, overIndex))
125 }
126 }
127 setActiveId(null)
128 }}
129 onDragCancel={() => setActiveId(null)}
130 collisionDetection={collisionDetection}
131 {...props}
132 >
133 <SortableContext items={value} strategy={strategy}>
134 {children}
135 </SortableContext>
136 {overlay ? (
137 <SortableOverlay activeId={activeId}>{overlay}</SortableOverlay>
138 ) : null}
139 </DndContext>
140 )
141}
142
143
144export { Sortable, SortableDragHandle, SortableItem, SortableOverlay }