Skip to content

Commit f89d9df

Browse files
committed
feat: draggable-list
1 parent e840f9c commit f89d9df

File tree

4 files changed

+198
-0
lines changed

4 files changed

+198
-0
lines changed

src/assets/svgs/drag-and-drop.svg

+3
Loading

src/assets/svgs/trash.svg

+3
Loading

src/lib/draggable-list/index.tsx

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useEffect } from "react";
2+
import { useListData } from "react-stately";
3+
import {
4+
Button,
5+
ListBox,
6+
ListBoxItem,
7+
useDragAndDrop,
8+
type ListBoxItemProps,
9+
type ListBoxProps,
10+
type DragAndDropOptions,
11+
} from "react-aria-components";
12+
import { cn } from "../../utils";
13+
import DragAndDropIcon from "../../assets/svgs/drag-and-drop.svg";
14+
import Trash from "../../assets/svgs/trash.svg";
15+
import clsx from "clsx";
16+
17+
type ListItem = {
18+
id: string | number;
19+
name: string;
20+
value: any;
21+
};
22+
interface IDraggableList
23+
extends Omit<
24+
ListBoxProps<ListBoxItemProps>,
25+
| "items"
26+
| "selectionMode"
27+
| "dragAndDropHooks"
28+
| "selectionBehavior"
29+
| "orientation"
30+
| "onSelectionChange"
31+
> {
32+
items: ListItem[];
33+
/** Returns the updated list after a delete or move operation. */
34+
updateCallback?: (updatedItems: ListItem[]) => void;
35+
/** Returns the selected item. */
36+
selectionCallback?: (list: ListItem) => void;
37+
/** Display custom preview for the item being dragged. */
38+
renderDragPreview?: DragAndDropOptions["renderDragPreview"];
39+
}
40+
41+
/** List that allows users to reorder items via drag and drop */
42+
function DraggableList({
43+
items,
44+
updateCallback,
45+
selectionCallback,
46+
className,
47+
renderDragPreview,
48+
...props
49+
}: Readonly<IDraggableList>) {
50+
const list = useListData({
51+
initialItems: items,
52+
});
53+
54+
useEffect(() => {
55+
if (!updateCallback) return;
56+
updateCallback(list.items);
57+
}, [list, updateCallback, items]);
58+
59+
const { dragAndDropHooks } = useDragAndDrop({
60+
getItems: (keys) =>
61+
[...keys].map((key) => ({ "text/plain": list.getItem(key)!.name })),
62+
getAllowedDropOperations: () => ["move"],
63+
onReorder(e) {
64+
if (e.target.dropPosition === "before") {
65+
list.moveBefore(e.target.key, e.keys);
66+
} else if (e.target.dropPosition === "after") {
67+
list.moveAfter(e.target.key, e.keys);
68+
}
69+
},
70+
renderDragPreview,
71+
});
72+
73+
return (
74+
<ListBox
75+
{...props}
76+
aria-label={props["aria-label"] ?? "Reorderable list"}
77+
selectionMode="single"
78+
items={list.items}
79+
dragAndDropHooks={dragAndDropHooks}
80+
onSelectionChange={(keys) => {
81+
const keyArr = Array.from(keys);
82+
const selectedItem = list.getItem(keyArr[0]);
83+
84+
if (selectionCallback && selectedItem) selectionCallback(selectedItem);
85+
}}
86+
className={cn(
87+
"bg-klerosUIComponentsLightBackground rounded-base border-klerosUIComponentsStroke border",
88+
"py-4",
89+
"[&_div]:data-drop-target:outline-klerosUIComponentsPrimaryBlue [&_div]:data-drop-target:outline",
90+
className,
91+
)}
92+
>
93+
{(item) => (
94+
<ListBoxItem
95+
textValue={item.name}
96+
className={({ isHovered, isDragging, isSelected }) =>
97+
cn(
98+
"h-11.25 w-95.5 cursor-pointer border-l-3 border-l-transparent",
99+
"flex items-center gap-4 px-4",
100+
"focus-visible:outline-klerosUIComponentsPrimaryBlue focus-visible:outline",
101+
(isHovered || isSelected) && "bg-klerosUIComponentsMediumBlue",
102+
isSelected && "border-l-klerosUIComponentsPrimaryBlue",
103+
isDragging && "cursor-grabbing opacity-60",
104+
)
105+
}
106+
>
107+
{({ isHovered }) => (
108+
<>
109+
<DragAndDropIcon className="size-4 cursor-grab" />
110+
<span className="text-klerosUIComponentsPrimaryText flex-1 text-base">
111+
{item.name}
112+
</span>
113+
{isHovered ? (
114+
<Button
115+
className={"cursor-pointer hover:scale-105"}
116+
onPress={() => {
117+
list.remove(item.id);
118+
}}
119+
>
120+
{({ isHovered: isButtonHovered }) => (
121+
<Trash
122+
className={clsx(
123+
"ease-ease size-4 transition",
124+
isButtonHovered &&
125+
"[&_path]:fill-klerosUIComponentsPrimaryBlue",
126+
)}
127+
/>
128+
)}
129+
</Button>
130+
) : null}
131+
</>
132+
)}
133+
</ListBoxItem>
134+
)}
135+
</ListBox>
136+
);
137+
}
138+
139+
export default DraggableList;
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import { IPreviewArgs } from "./utils";
5+
6+
import DraggableList from "../lib/draggable-list";
7+
8+
const meta = {
9+
component: DraggableList,
10+
title: "Draggable List",
11+
tags: ["autodocs"],
12+
} satisfies Meta<typeof DraggableList>;
13+
14+
export default meta;
15+
16+
type Story = StoryObj<typeof meta> & IPreviewArgs;
17+
18+
export const Default: Story = {
19+
args: {
20+
themeUI: "light",
21+
backgroundUI: "light",
22+
items: [
23+
{ id: 1, name: "Illustrator", value: "" },
24+
{ id: 2, name: "Premiere", value: "" },
25+
{ id: 3, name: "Acrobat", value: "" },
26+
],
27+
},
28+
render: (args) => {
29+
return <DraggableList {...args} />;
30+
},
31+
};
32+
33+
export const CustomDragPreview: Story = {
34+
args: {
35+
themeUI: "light",
36+
backgroundUI: "light",
37+
items: [
38+
{ id: 1, name: "Illustrator", value: "" },
39+
{ id: 2, name: "Premiere", value: "" },
40+
{ id: 3, name: "Acrobat", value: "" },
41+
],
42+
renderDragPreview: (items) => (
43+
<div className="rounded-base bg-klerosUIComponentsPrimaryBlue px-4 py-2">
44+
<span className="text-klerosUIComponentsPrimaryText text-base">
45+
{items[0]["text/plain"]}
46+
</span>
47+
</div>
48+
),
49+
},
50+
render: (args) => {
51+
return <DraggableList {...args} />;
52+
},
53+
};

0 commit comments

Comments
 (0)