Skip to content

Commit 9f418f8

Browse files
committed
support setting drag preview (#115)
1 parent db406e5 commit 9f418f8

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

.changeset/poor-coats-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@headless-tree/core": patch
3+
---
4+
5+
support setting the drag preview with the `setDragImage` option (#115)

packages/core/src/features/drag-and-drop/feature.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ export const dragAndDropFeature: FeatureImplementation = {
117117
return;
118118
}
119119

120+
if (config.setDragImage) {
121+
const { imgElement, xOffset, yOffset } = config.setDragImage(items);
122+
e.dataTransfer?.setDragImage(imgElement, xOffset ?? 0, yOffset ?? 0);
123+
}
124+
120125
if (config.createForeignDragObject) {
121126
const { format, data } = config.createForeignDragObject(items);
122127
e.dataTransfer?.setData(format, data);

packages/core/src/features/drag-and-drop/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export type DragAndDropFeatureDef<T> = {
5858
format: string;
5959
data: any;
6060
};
61+
setDragImage?: (items: ItemInstance<T>[]) => {
62+
imgElement: Element;
63+
xOffset?: number;
64+
yOffset?: number;
65+
};
6166
canDropForeignDragObject?: (
6267
dataTransfer: DataTransfer,
6368
target: DragTarget<T>,

packages/docs/docs/3-dnd/3-customizability.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,20 @@ instead.
4747

4848
<DemoBox tags={["feature/dnd"]} initialStory="react-drag-and-drop-can-drop--can-drop" />
4949

50+
## Custom Drag Preview
51+
52+
You can customize the drag preview by providing a `setDragImage` to your tree options. It will be called
53+
when the user starts dragging, with the dragged items as parameter. The functions return parameters will
54+
be used to invoke [`dataTransfer.setDragImage`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage)
55+
to set the drag preview.
56+
57+
```ts
58+
setDragImage: (draggedItems) => ({
59+
imgElement: document.getElementById("dragpreview"),
60+
})
61+
```
62+
63+
The provided element does not have to be an image element, but if it is not, it will need to be visible in the viewport.
64+
In this case, you will want to move it with CSS off-screen to hide it whenever the user is not dragging anything.
65+
66+
<DemoBox tags={["feature/dnd"]} initialStory="react-drag-and-drop-drag-preview--drag-preview" />
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Meta } from "@storybook/react";
2+
import React, { useState } from "react";
3+
import {
4+
TreeState,
5+
dragAndDropFeature,
6+
hotkeysCoreFeature,
7+
keyboardDragAndDropFeature,
8+
selectionFeature,
9+
syncDataLoaderFeature,
10+
} from "@headless-tree/core";
11+
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
12+
import cn from "classnames";
13+
14+
const meta = {
15+
title: "React/Drag and Drop/Drag Preview",
16+
tags: ["feature/dnd"],
17+
} satisfies Meta;
18+
19+
export default meta;
20+
21+
// story-start
22+
export const DragPreview = () => {
23+
const [state, setState] = useState<Partial<TreeState<any>>>({
24+
expandedItems: ["root-1", "root-1-2"],
25+
selectedItems: ["root-1-2-1", "root-1-2-2", "root-1-2-3", "root-1-2-4"],
26+
});
27+
const tree = useTree<string>({
28+
state,
29+
setState,
30+
rootItemId: "root",
31+
getItemName: (item) => item.getItemData(),
32+
isItemFolder: () => true,
33+
canReorder: true,
34+
onDrop: (items, target) => {
35+
alert(
36+
`Dropped ${items.map((item) =>
37+
item.getId(),
38+
)} on ${target.item.getId()}, ${JSON.stringify(target)}`,
39+
);
40+
},
41+
setDragImage: () => ({
42+
imgElement: document.getElementById("dragpreview")!,
43+
xOffset: -20,
44+
}),
45+
indent: 20,
46+
dataLoader: {
47+
getItem: (itemId) => itemId,
48+
getChildren: (itemId) => [
49+
`${itemId}-1`,
50+
`${itemId}-2`,
51+
`${itemId}-3`,
52+
`${itemId}-4`,
53+
],
54+
},
55+
features: [
56+
syncDataLoaderFeature,
57+
selectionFeature,
58+
hotkeysCoreFeature,
59+
dragAndDropFeature,
60+
keyboardDragAndDropFeature,
61+
],
62+
});
63+
64+
const draggedItems = tree.getState().dnd?.draggedItems;
65+
66+
return (
67+
<>
68+
<p className="description">
69+
This sample utilizes custom drag previews that are defined with the
70+
option setDragImage. Start dragging items to see the custom preview.
71+
</p>
72+
<div {...tree.getContainerProps()} className="tree">
73+
<AssistiveTreeDescription tree={tree} />
74+
{tree.getItems().map((item) => (
75+
<button
76+
{...item.getProps()}
77+
key={item.getId()}
78+
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
79+
>
80+
<div
81+
className={cn("treeitem", {
82+
focused: item.isFocused(),
83+
expanded: item.isExpanded(),
84+
selected: item.isSelected(),
85+
folder: item.isFolder(),
86+
drop: item.isDragTarget(),
87+
})}
88+
>
89+
{item.getItemName()}
90+
</div>
91+
</button>
92+
))}
93+
<div style={tree.getDragLineStyle()} className="dragline" />
94+
<div
95+
id="dragpreview"
96+
style={{
97+
// move the drag preview off-screen by default
98+
position: "absolute",
99+
left: "-9999px",
100+
101+
// styling to make it look nice
102+
width: "200px",
103+
background: "white",
104+
borderRadius: "4px",
105+
border: "1px solid #eee",
106+
padding: "4px",
107+
}}
108+
>
109+
Dragging{" "}
110+
{draggedItems
111+
?.slice(0, 3)
112+
.map((item) => item.getItemName())
113+
.join(", ")}
114+
{(draggedItems?.length ?? 0) > 3 &&
115+
` and ${(draggedItems?.length ?? 0) - 3} more`}
116+
</div>
117+
</div>
118+
</>
119+
);
120+
};

0 commit comments

Comments
 (0)