diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ba6c251c2..771de7189 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -18,11 +18,8 @@ jobs: run: yarn - name: Custom React version run: | - echo "enableImmutableInstalls: false" > ./.yarnrc.yml - yarn add react@${{ matrix.react }} -D - cd packages/core - yarn add react@${{ matrix.react }} -D - cd ../.. + echo -e "nodeLinker: node-modules\nenableImmutableInstalls: false" > ./.yarnrc.yml + npm pkg set devDependencies.react=${{matrix.react}} --ws cat ./.yarnrc.yml yarn - name: Build diff --git a/next-release-notes.md b/next-release-notes.md index f77d5bd6a..61f2df7c6 100644 --- a/next-release-notes.md +++ b/next-release-notes.md @@ -1,9 +1,3 @@ - \ No newline at end of file +- Don't show drag line when dragging outside of the tree container (#417) +- Fix a bug where items where dropped on the last valid position when dragging items on an invalid position and then dropping (#417) \ No newline at end of file diff --git a/package.json b/package.json index 59a23b1be..a51e87722 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "repository": { "type": "git", "url": "git@github.com:lukasbach/react-complex-tree.git", - "directory": "packages/docs" + "directory": "." }, "author": "Lukas Bach", "license": "MIT", diff --git a/packages/core/package.json b/packages/core/package.json index cbeec69e4..9a9982f1f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,5 +66,9 @@ "setupFiles": [ "./test/helpers/setup.ts" ] + }, + "volta": { + "node": "18.12.1", + "yarn": "3.3.0" } } diff --git a/packages/core/src/controlledEnvironment/layoutUtils.ts b/packages/core/src/controlledEnvironment/layoutUtils.ts index d8e392b5a..ec161f3df 100644 --- a/packages/core/src/controlledEnvironment/layoutUtils.ts +++ b/packages/core/src/controlledEnvironment/layoutUtils.ts @@ -16,7 +16,7 @@ export const computeItemHeight = (treeId: string) => { }; export const isOutsideOfContainer = (e: DragEvent, treeBb: DOMRect) => - e.clientX < treeBb.left || - e.clientX > treeBb.right || - e.clientY < treeBb.top || - e.clientY > treeBb.bottom; + e.clientX <= treeBb.left || + e.clientX >= treeBb.right || + e.clientY <= treeBb.top || + e.clientY >= treeBb.bottom; diff --git a/packages/core/src/drag/DragAndDropProvider.tsx b/packages/core/src/drag/DragAndDropProvider.tsx index 101c9f5ad..f55174784 100644 --- a/packages/core/src/drag/DragAndDropProvider.tsx +++ b/packages/core/src/drag/DragAndDropProvider.tsx @@ -15,6 +15,7 @@ import { useCallSoon } from '../useCallSoon'; import { useStableHandler } from '../useStableHandler'; import { useGetOriginalItemOrder } from '../useGetOriginalItemOrder'; import { useDraggingPosition } from './useDraggingPosition'; +import { isOutsideOfContainer } from '../controlledEnvironment/layoutUtils'; const DragAndDropContext = React.createContext( null as any @@ -177,6 +178,20 @@ export const DragAndDropProvider: React.FC = ({ } ); + const onDragLeaveContainerHandler = useStableHandler( + ( + e: DragEvent, + containerRef: React.MutableRefObject + ) => { + if (!containerRef.current) return; + if ( + isOutsideOfContainer(e, containerRef.current.getBoundingClientRect()) + ) { + setDraggingPosition(undefined); + } + } + ); + const onDropHandler = useStableHandler(() => { if (!draggingItems || !draggingPosition || !environment.onDrop) { return; @@ -287,6 +302,7 @@ export const DragAndDropProvider: React.FC = ({ itemHeight: itemHeight.current, isProgrammaticallyDragging, onDragOverTreeHandler, + onDragLeaveContainerHandler, viableDragPositions, }), [ @@ -297,6 +313,7 @@ export const DragAndDropProvider: React.FC = ({ isProgrammaticallyDragging, itemHeight, onDragOverTreeHandler, + onDragLeaveContainerHandler, onStartDraggingItems, programmaticDragDown, programmaticDragUp, diff --git a/packages/core/src/drag/DraggingPositionEvaluation.ts b/packages/core/src/drag/DraggingPositionEvaluation.ts index 442447802..b3e85b266 100644 --- a/packages/core/src/drag/DraggingPositionEvaluation.ts +++ b/packages/core/src/drag/DraggingPositionEvaluation.ts @@ -227,7 +227,7 @@ export class DraggingPositionEvaluation { return true; } - getDraggingPosition(): DraggingPosition | undefined { + getDraggingPosition(): DraggingPosition | 'invalid' | undefined { if (this.env.linearItems[this.treeId].length === 0) { return this.getEmptyTreeDragPosition(); } @@ -251,11 +251,11 @@ export class DraggingPositionEvaluation { } if (this.areDraggingItemsDescendantOfTarget()) { - return undefined; + return 'invalid'; } if (!this.canDropAtCurrentTarget()) { - return undefined; + return 'invalid'; } const { parent } = this.getParentOfLinearItem( diff --git a/packages/core/src/tree/TreeManager.tsx b/packages/core/src/tree/TreeManager.tsx index 890a7455a..cd45eb856 100644 --- a/packages/core/src/tree/TreeManager.tsx +++ b/packages/core/src/tree/TreeManager.tsx @@ -48,6 +48,9 @@ export const TreeManager = (): JSX.Element => { e.preventDefault(); // Allow drop. Also implicitly set by items, but needed here as well for dropping on empty space dnd.onDragOverTreeHandler(e as any, treeId, containerRef); }, + onDragLeave: e => { + dnd.onDragLeaveContainerHandler(e as any, containerRef); + }, onMouseDown: () => dnd.abortProgrammaticDrag(), ref: containerRef, style: { position: 'relative' }, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c8023fb8d..421eff276 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -328,6 +328,10 @@ export interface DragAndDropContextProps { treeId: string, containerRef: React.MutableRefObject ) => void; + onDragLeaveContainerHandler: ( + e: DragEvent, + containerRef: React.MutableRefObject + ) => void; } export type DraggingPosition = diff --git a/packages/core/test/dnd-basics.spec.tsx b/packages/core/test/dnd-basics.spec.tsx index b4e4b4e18..4303ad5b8 100644 --- a/packages/core/test/dnd-basics.spec.tsx +++ b/packages/core/test/dnd-basics.spec.tsx @@ -654,4 +654,35 @@ describe('dnd basics', () => { }); }); }); + + it('doesnt drop on last valid location when moving drop to invalid location', async () => { + const test = await new TestUtil().renderOpenTree({}); + await test.startDrag('aab'); + await test.dragOver('aa'); + await test.dragOver('aab'); + await test.drop(); + await test.expectItemContentsUnchanged('aa'); + }); + + it('sets drop position correctly', async () => { + const test = await new TestUtil().renderOpenTree({}); + await test.startDrag('target'); + await test.dragOver('aa'); + expect(test.treeRef?.dragAndDropContext?.draggingPosition).toStrictEqual({ + depth: 1, + linearIndex: 5, + parentItem: 'a', + targetItem: 'aa', + targetType: 'item', + treeId: 'tree-1', + }); + }); + + it('unsets drop position when dragging out', async () => { + const test = await new TestUtil().renderOpenTree({}); + await test.startDrag('target'); + await test.dragOver('aa'); + await test.dragLeave(); + expect(test.treeRef?.dragAndDropContext?.draggingPosition).toBe(undefined); + }); }); diff --git a/packages/core/test/helpers/TestUtil.tsx b/packages/core/test/helpers/TestUtil.tsx index 4c5105847..1903ee804 100644 --- a/packages/core/test/helpers/TestUtil.tsx +++ b/packages/core/test/helpers/TestUtil.tsx @@ -171,6 +171,20 @@ export class TestUtil { }); } + public async dragLeave() { + (isOutsideOfContainer as jest.Mock).mockReturnValue(true); + await act(async () => { + this.environmentRef?.dragAndDropContext.onDragLeaveContainerHandler( + { + clientX: 9999, + clientY: 9999, + } as any, + { current: this.containerRef ?? undefined } + ); + }); + (isOutsideOfContainer as jest.Mock).mockReturnValue(false); + } + public async drop() { await act(async () => { fireEvent.drop(window);