Skip to content

Commit fbd7aee

Browse files
committed
feat: dropping items on empty trees (#182)
1 parent a867ca8 commit fbd7aee

File tree

10 files changed

+144
-18
lines changed

10 files changed

+144
-18
lines changed

packages/core/src/controlledEnvironment/DragAndDropProvider.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export const DragAndDropProvider: React.FC<React.PropsWithChildren> = ({
161161
const onDragOverTreeHandler = useOnDragOverTreeHandler(
162162
dragCode,
163163
setDragCode,
164+
draggingItems,
164165
itemHeight,
165166
setDraggingPosition,
166167
performDrag

packages/core/src/controlledEnvironment/useCanDropAt.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const useCanDropAt = () => {
1111
if (!environment.canReorderItems) {
1212
return false;
1313
}
14+
} else if (draggingPosition.targetType === 'root') {
15+
if (!environment.canDropOnFolder) {
16+
return false;
17+
}
1418
} else {
1519
const resolvedItem = environment.items[draggingPosition.targetItem];
1620
if (

packages/core/src/controlledEnvironment/useOnDragOverTreeHandler.ts

+37-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ const getHoveringPosition = (
2222
) => {
2323
const hoveringPosition = (clientY - treeTop) / itemHeight;
2424

25+
const treeLinearItems = linearItems[treeId];
2526
const linearIndex = Math.floor(hoveringPosition);
26-
const targetLinearItem = linearItems[treeId][linearIndex];
27+
28+
if (linearIndex > treeLinearItems.length - 1) {
29+
return {
30+
linearIndex: treeLinearItems.length - 1,
31+
targetItem: treeLinearItems[treeLinearItems.length - 1].item,
32+
offset: 'bottom',
33+
targetLinearItem: treeLinearItems[treeLinearItems.length - 1],
34+
} as const;
35+
}
36+
37+
const targetLinearItem = treeLinearItems[linearIndex];
2738
const targetItem = items[targetLinearItem.item];
2839
let offset: 'top' | 'bottom' | undefined;
2940

@@ -42,6 +53,7 @@ const getHoveringPosition = (
4253
export const useOnDragOverTreeHandler = (
4354
lastDragCode: string,
4455
setLastDragCode: (code: string) => void,
56+
draggingItems: TreeItem[] | undefined,
4557
itemHeight: number,
4658
onDragAtPosition: (draggingPosition: DraggingPosition | undefined) => void,
4759
onPerformDrag: (draggingPosition: DraggingPosition) => void
@@ -53,7 +65,7 @@ export const useOnDragOverTreeHandler = (
5365
linearItems,
5466
items,
5567
canReorderItems,
56-
viewState,
68+
trees,
5769
} = useTreeEnvironment();
5870
const getParentOfLinearItem = useGetGetParentOfLinearItem();
5971

@@ -63,6 +75,10 @@ export const useOnDragOverTreeHandler = (
6375
treeId: string,
6476
containerRef: React.MutableRefObject<HTMLElement | undefined>
6577
) => {
78+
if (!draggingItems) {
79+
return;
80+
}
81+
6682
if (!canDragAndDrop) {
6783
return;
6884
}
@@ -78,6 +94,18 @@ export const useOnDragOverTreeHandler = (
7894
const treeBb = containerRef.current.getBoundingClientRect();
7995
const outsideContainer = isOutsideOfContainer(e, treeBb);
8096

97+
if (linearItems[treeId].length === 0) {
98+
// Empty tree
99+
onPerformDrag({
100+
targetType: 'root',
101+
treeId,
102+
depth: 0,
103+
linearIndex: 0,
104+
targetItem: trees[treeId].rootItem,
105+
});
106+
return;
107+
}
108+
81109
let { linearIndex, offset } = getHoveringPosition(
82110
e.clientY,
83111
treeBb.top,
@@ -130,7 +158,11 @@ export const useOnDragOverTreeHandler = (
130158

131159
const parent = getParentOfLinearItem(linearIndex, treeId);
132160

133-
if (viewState[treeId]?.selectedItems?.includes(targetItem.item)) {
161+
if (
162+
draggingItems.some(
163+
draggingItem => draggingItem.index === targetItem.item
164+
)
165+
) {
134166
return;
135167
}
136168

@@ -178,6 +210,7 @@ export const useOnDragOverTreeHandler = (
178210
canDropOnFolder,
179211
canDropOnNonFolder,
180212
canReorderItems,
213+
draggingItems,
181214
getParentOfLinearItem,
182215
itemHeight,
183216
items,
@@ -186,7 +219,7 @@ export const useOnDragOverTreeHandler = (
186219
onDragAtPosition,
187220
onPerformDrag,
188221
setLastDragCode,
189-
viewState,
222+
trees,
190223
]
191224
);
192225
};

packages/core/src/renderers/createDefaultRenderers.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ export const createDefaultRenderers = (
173173
info.areItemsSelected && 'rct-tree-root-itemsselected'
174174
)}
175175
>
176-
<div {...containerProps}>{children}</div>
176+
<div
177+
{...containerProps}
178+
style={{ minHeight: '30px', ...containerProps.style }}
179+
>
180+
{children}
181+
</div>
177182
</div>
178183
),
179184
renderItemsContainer: ({ children, containerProps }) => (

packages/core/src/stories/BasicExamples.stories.tsx

+72
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,78 @@ export const MultipleTrees = () => (
206206
</UncontrolledTreeEnvironment>
207207
);
208208

209+
export const DropOnEmptyTree = () => (
210+
<UncontrolledTreeEnvironment<string>
211+
canDragAndDrop
212+
canDropOnFolder
213+
canReorderItems
214+
dataProvider={
215+
new StaticTreeDataProvider(
216+
{
217+
...longTree.items,
218+
empty: {
219+
data: 'Empty Folder',
220+
index: 'empty',
221+
isFolder: true,
222+
},
223+
},
224+
(item, data) => ({
225+
...item,
226+
data,
227+
})
228+
)
229+
}
230+
getItemTitle={item => item.data}
231+
viewState={{
232+
'tree-1': {},
233+
}}
234+
renderTreeContainer={({ children, containerProps, info }) => (
235+
<div
236+
className={[
237+
'rct-tree-root',
238+
info.isFocused && 'rct-tree-root-focus',
239+
info.isRenaming && 'rct-tree-root-renaming',
240+
info.areItemsSelected && 'rct-tree-root-itemsselected',
241+
].join(' ')}
242+
>
243+
<div
244+
{...containerProps}
245+
style={{ minHeight: '400px', ...containerProps.style }}
246+
>
247+
{children}
248+
</div>
249+
</div>
250+
)}
251+
>
252+
<div
253+
style={{
254+
display: 'flex',
255+
backgroundColor: '#D8DEE9',
256+
justifyContent: 'space-evenly',
257+
alignItems: 'center',
258+
padding: '20px 0',
259+
}}
260+
>
261+
<div
262+
style={{
263+
width: '28%',
264+
backgroundColor: 'white',
265+
}}
266+
>
267+
<Tree treeId="tree-1" rootItem="root" treeLabel="Tree 1" />
268+
</div>
269+
<div
270+
style={{
271+
width: '28%',
272+
backgroundColor: 'white',
273+
}}
274+
>
275+
<Tree treeId="tree-2" rootItem="empty" treeLabel="Tree 2" />
276+
</div>
277+
</div>
278+
</UncontrolledTreeEnvironment>
279+
);
280+
209281
export const SwitchMountedTree = () => {
210282
const [showFirstTree, setShowFirstTree] = useState(false);
211283
return (

packages/core/src/tree/TreeManager.tsx

+5-7
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,22 @@ export const TreeManager = (): JSX.Element => {
3232

3333
const rootChildren = environment.items[rootItem].children;
3434

35-
if (!rootChildren) {
36-
throw Error(`Root ${rootItem} does not contain any children`);
37-
}
38-
3935
const treeChildren = (
4036
<>
4137
<MaybeLiveDescription />
4238
<TreeItemChildren depth={0} parentId={treeId}>
43-
{rootChildren}
39+
{rootChildren ?? []}
4440
</TreeItemChildren>
4541
<DragBetweenLine treeId={treeId} />
4642
<SearchInput containerRef={containerRef.current} />
4743
</>
4844
);
4945

5046
const containerProps: HTMLProps<any> = {
51-
// onDragOver: createOnDragOverHandler(environment, containerRef, lastHoverCode, getLinearItems, rootItem, treeId),
52-
onDragOver: e => dnd.onDragOverTreeHandler(e as any, treeId, containerRef),
47+
onDragOver: e => {
48+
e.preventDefault(); // Allow drop. Also implicitly set by items, but needed here as well for dropping on empty space
49+
dnd.onDragOverTreeHandler(e as any, treeId, containerRef);
50+
},
5351
onMouseDown: () => dnd.abortProgrammaticDrag(),
5452
ref: containerRef,
5553
style: { position: 'relative' },

packages/core/src/tree/resolveLiveDescriptor.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ export const resolveLiveDescriptor = (
3333
if (!dnd.draggingPosition) {
3434
return 'None';
3535
}
36-
if (dnd.draggingPosition.targetType === 'item') {
36+
if (
37+
dnd.draggingPosition.targetType === 'item' ||
38+
dnd.draggingPosition.targetType === 'root'
39+
) {
3740
return `within ${getItemTitle(dnd.draggingPosition.targetItem)}`;
3841
}
3942
const parentItem = environment.items[dnd.draggingPosition.parentItem];

packages/core/src/types.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -300,25 +300,32 @@ export interface DragAndDropContextProps<T = any> {
300300

301301
export type DraggingPosition =
302302
| DraggingPositionItem
303-
| DraggingPositionBetweenItems;
303+
| DraggingPositionBetweenItems
304+
| DreaggingPositionRoot;
304305

305306
export interface AbstractDraggingPosition {
306-
targetType: 'item' | 'between-items';
307+
targetType: 'item' | 'between-items' | 'root';
307308
treeId: string;
308-
parentItem: TreeItemIndex;
309309
linearIndex: number;
310310
depth: number;
311311
}
312312

313313
export interface DraggingPositionItem extends AbstractDraggingPosition {
314314
targetType: 'item';
315315
targetItem: TreeItemIndex;
316+
parentItem: TreeItemIndex;
316317
}
317318

318319
export interface DraggingPositionBetweenItems extends AbstractDraggingPosition {
319320
targetType: 'between-items';
320321
childIndex: number;
321322
linePosition: 'top' | 'bottom';
323+
parentItem: TreeItemIndex;
324+
}
325+
326+
export interface DreaggingPositionRoot extends AbstractDraggingPosition {
327+
targetType: 'root';
328+
targetItem: TreeItemIndex;
322329
}
323330

324331
export interface ControlledTreeEnvironmentProps<

packages/core/src/uncontrolledEnvironment/UncontrolledTreeEnvironment.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ export const UncontrolledTreeEnvironment = React.forwardRef<
126126
const parent = Object.values(currentItems).find(potentialParent =>
127127
potentialParent.children?.includes(item.index)
128128
);
129-
const newParent = currentItems[target.parentItem];
130129

131130
if (!parent) {
132131
throw Error(`Could not find parent of item "${item.index}"`);
@@ -138,7 +137,7 @@ export const UncontrolledTreeEnvironment = React.forwardRef<
138137
);
139138
}
140139

141-
if (target.targetType === 'item') {
140+
if (target.targetType === 'item' || target.targetType === 'root') {
142141
if (target.targetItem === parent.index) {
143142
// NOOP
144143
} else {
@@ -156,6 +155,7 @@ export const UncontrolledTreeEnvironment = React.forwardRef<
156155
);
157156
}
158157
} else {
158+
const newParent = currentItems[target.parentItem];
159159
const newParentChildren = [...(newParent.children ?? [])].filter(
160160
child => child !== item.index
161161
);

packages/docs/docs/changelog.mdx

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ were renamed alongside to match.
3030
the previous selection range should be cleared before selecting the new range.
3131
- Removed unnecessary memoization for event handlers, favoring stable callbacks stored in refs instead.
3232
- Fixed a bug where renaming an item would make the page loose focus of the active tree.
33+
- Items can now be dropped on empty trees. The tree container will need to be rendered with a minimum height for this
34+
to work, in the default renderers the container will now render with a minimum height of 30px, which can be adjusted
35+
in custom renderers.
3336

3437
### Other Changes
3538

0 commit comments

Comments
 (0)