-
Draggable
+
+
+
Draggable
+
+ {draggableProvided.placeholder}
)}
diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js
index b7586847e3..452f645350 100644
--- a/test/unit/state/dimension.spec.js
+++ b/test/unit/state/dimension.spec.js
@@ -4,9 +4,10 @@ import {
getDroppableDimension,
} from '../../../src/state/dimension';
import { vertical, horizontal } from '../../../src/state/axis';
-// eslint-disable-next-line no-duplicate-imports
-import type { Margin, ClientRect } from '../../../src/state/dimension';
+import getClientRect from '../../../src/state/get-client-rect';
import type {
+ ClientRect,
+ Spacing,
DraggableId,
DroppableId,
Position,
@@ -26,14 +27,22 @@ const clientRect: ClientRect = {
width: 90,
height: 80,
};
-const margin: Margin = {
+const margin: Spacing = {
top: 1, right: 2, bottom: 3, left: 4,
};
+const padding: Spacing = {
+ top: 5, right: 6, bottom: 7, left: 8,
+};
const windowScroll: Position = {
x: 50,
y: 80,
};
+const getCenter = (rect: ClientRect): Position => ({
+ x: (rect.left + rect.right) / 2,
+ y: (rect.top + rect.bottom) / 2,
+});
+
describe('dimension', () => {
describe('draggable dimension', () => {
const dimension: DraggableDimension = getDraggableDimension({
@@ -44,7 +53,7 @@ describe('dimension', () => {
windowScroll,
});
- describe('client coordinates', () => {
+ describe('without scroll (client)', () => {
it('should return a portion that does not account for margins', () => {
const fragment: DimensionFragment = {
top: clientRect.top,
@@ -62,23 +71,27 @@ describe('dimension', () => {
});
it('should return a portion that considers margins', () => {
- const fragment: DimensionFragment = {
+ const rect: ClientRect = getClientRect({
top: clientRect.top + margin.top,
right: clientRect.right + margin.right,
bottom: clientRect.bottom + margin.bottom,
left: clientRect.left + margin.left,
- width: clientRect.width + margin.left + margin.right,
- height: clientRect.height + margin.top + margin.bottom,
- center: {
- x: (clientRect.left + margin.left + clientRect.right + margin.right) / 2,
- y: (clientRect.top + margin.top + clientRect.bottom + margin.bottom) / 2,
- },
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
};
expect(dimension.client.withMargin).toEqual(fragment);
});
});
- describe('page coordination', () => {
+ describe('with scroll (page)', () => {
it('should return a portion that does not account for margins', () => {
const top: number = clientRect.top + windowScroll.y;
const right: number = clientRect.right + windowScroll.x;
@@ -101,22 +114,21 @@ describe('dimension', () => {
});
it('should return a portion that considers margins', () => {
- const top: number = clientRect.top + margin.top + windowScroll.y;
- const right: number = clientRect.right + margin.right + windowScroll.x;
- const bottom: number = clientRect.bottom + margin.bottom + windowScroll.y;
- const left: number = clientRect.left + margin.left + windowScroll.x;
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + margin.top + windowScroll.y,
+ right: clientRect.right + margin.right + windowScroll.x,
+ bottom: clientRect.bottom + margin.bottom + windowScroll.y,
+ left: clientRect.left + margin.left + windowScroll.x,
+ });
const fragment: DimensionFragment = {
- top,
- right,
- bottom,
- left,
- width: clientRect.width + margin.left + margin.right,
- height: clientRect.height + margin.top + margin.bottom,
- center: {
- x: (left + right) / 2,
- y: (top + bottom) / 2,
- },
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
};
expect(dimension.page.withMargin).toEqual(fragment);
@@ -134,6 +146,7 @@ describe('dimension', () => {
id: droppableId,
clientRect,
margin,
+ padding,
windowScroll,
scroll,
});
@@ -178,48 +191,127 @@ describe('dimension', () => {
expect(withHorizontal.axis).toBe(horizontal);
});
- it('should return a portion that does not consider margins', () => {
- const top: number = clientRect.top + windowScroll.y;
- const left: number = clientRect.left + windowScroll.x;
- const bottom: number = clientRect.bottom + windowScroll.y;
- const right: number = clientRect.right + windowScroll.x;
-
- const fragment: DimensionFragment = {
- top,
- right,
- bottom,
- left,
- width: clientRect.width,
- height: clientRect.height,
- center: {
- x: (left + right) / 2,
- y: (top + bottom) / 2,
- },
- };
-
- expect(dimension.page.withoutMargin).toEqual(fragment);
+ describe('without scroll (client)', () => {
+ it('should return a portion that does not consider margins', () => {
+ const fragment: DimensionFragment = {
+ top: clientRect.top,
+ right: clientRect.right,
+ bottom: clientRect.bottom,
+ left: clientRect.left,
+ width: clientRect.width,
+ height: clientRect.height,
+ center: getCenter(clientRect),
+ };
+
+ expect(dimension.client.withoutMargin).toEqual(fragment);
+ });
+
+ it('should return a portion that does consider margins', () => {
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + margin.top,
+ left: clientRect.left + margin.left,
+ bottom: clientRect.bottom + margin.bottom,
+ right: clientRect.right + margin.right,
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
+ };
+
+ expect(dimension.client.withMargin).toEqual(fragment);
+ });
+
+ it('should return a portion that considers margins and padding', () => {
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + margin.top + padding.top,
+ left: clientRect.left + margin.left + padding.left,
+ bottom: clientRect.bottom + margin.bottom + padding.bottom,
+ right: clientRect.right + margin.right + padding.right,
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
+ };
+
+ expect(dimension.client.withMarginAndPadding).toEqual(fragment);
+ });
});
- it('should return a portion that does consider margins', () => {
- const top: number = clientRect.top + windowScroll.y + margin.top;
- const left: number = clientRect.left + windowScroll.x + margin.left;
- const bottom: number = clientRect.bottom + windowScroll.y + margin.bottom;
- const right: number = clientRect.right + windowScroll.x + margin.right;
-
- const fragment: DimensionFragment = {
- top,
- right,
- bottom,
- left,
- width: clientRect.width + margin.left + margin.right,
- height: clientRect.height + margin.top + margin.bottom,
- center: {
- x: (left + right) / 2,
- y: (top + bottom) / 2,
- },
- };
-
- expect(dimension.page.withMargin).toEqual(fragment);
+ describe('with scroll (page)', () => {
+ it('should return a portion that does not consider margins', () => {
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + windowScroll.y,
+ left: clientRect.left + windowScroll.x,
+ bottom: clientRect.bottom + windowScroll.y,
+ right: clientRect.right + windowScroll.x,
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
+ };
+
+ expect(dimension.page.withoutMargin).toEqual(fragment);
+ });
+
+ it('should return a portion that does consider margins', () => {
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + windowScroll.y + margin.top,
+ left: clientRect.left + windowScroll.x + margin.left,
+ bottom: clientRect.bottom + windowScroll.y + margin.bottom,
+ right: clientRect.right + windowScroll.x + margin.right,
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
+ };
+
+ expect(dimension.page.withMargin).toEqual(fragment);
+ });
+
+ it('should return a portion that considers margins and padding', () => {
+ const rect: ClientRect = getClientRect({
+ top: clientRect.top + windowScroll.y + margin.top + padding.top,
+ left: clientRect.left + windowScroll.x + margin.left + padding.left,
+ bottom: clientRect.bottom + windowScroll.y + margin.bottom + padding.bottom,
+ right: clientRect.right + windowScroll.x + margin.right + padding.right,
+ });
+
+ const fragment: DimensionFragment = {
+ top: rect.top,
+ right: rect.right,
+ bottom: rect.bottom,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ center: getCenter(rect),
+ };
+
+ expect(dimension.page.withMarginAndPadding).toEqual(fragment);
+ });
});
});
});
diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js
index 2cc6dc4829..448543cd06 100644
--- a/test/unit/state/get-drag-impact.spec.js
+++ b/test/unit/state/get-drag-impact.spec.js
@@ -5,8 +5,10 @@ import {
} from '../../../src/state/dimension';
// eslint-disable-next-line no-duplicate-imports
import getDragImpact from '../../../src/state/get-drag-impact';
-import noImpact from '../../../src/state/no-impact';
-import getClientRect from '../../utils/get-client-rect';
+import noImpact, { noMovement } from '../../../src/state/no-impact';
+import getClientRect from '../../../src/state/get-client-rect';
+import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables';
+import { add, patch } from '../../../src/state/position';
import type {
WithinDroppable,
DroppableId,
@@ -83,7 +85,7 @@ describe('get drag impact', () => {
};
it('should return no movement when not dragging over anything', () => {
- // dragging up above the list
+ // dragging up above the list
const page: Position = {
x: droppable.page.withMargin.left,
y: droppable.page.withMargin.top - 100,
@@ -107,7 +109,7 @@ describe('get drag impact', () => {
describe('moving forward', () => {
describe('not moved far enough', () => {
it('should return the starting position', () => {
- // moving forward - but not enough
+ // moving forward - but not enough
const page: Position = {
x: draggable2.page.withoutMargin.center.x,
y: draggable2.page.withoutMargin.center.y + 1,
@@ -141,7 +143,7 @@ describe('get drag impact', () => {
});
describe('moving past one item', () => {
- // moving forward past the top of the next item
+ // moving forward past the top of the next item
const page: Position = {
x: draggable1.page.withoutMargin.center.x,
y: draggable2.page.withoutMargin.top + 1,
@@ -190,7 +192,7 @@ describe('get drag impact', () => {
});
describe('moving past two items', () => {
- // moving forward past the top of the third item
+ // moving forward past the top of the third item
const page: Position = {
x: draggable1.page.withoutMargin.center.x,
y: draggable3.page.withoutMargin.top + 1,
@@ -233,13 +235,13 @@ describe('get drag impact', () => {
expect(impact.movement.amount).toEqual(expected);
});
- it('should return the items that need to be moved', () => {
- expect(impact.movement.draggables).toEqual([draggable2.id, draggable3.id]);
+ it('should return the items that need to be moved (sorted by the closest to the draggables current location)', () => {
+ expect(impact.movement.draggables).toEqual([draggable3.id, draggable2.id]);
});
});
describe('moving past one item when the dragging item is not the first in the list', () => {
- // moving the second item forward past the top of the third item
+ // moving the second item forward past the top of the third item
const page: Position = {
x: draggable2.page.withoutMargin.center.x,
y: draggable3.page.withMargin.top + 1,
@@ -288,7 +290,7 @@ describe('get drag impact', () => {
});
describe('moving past an item due to change in droppable scroll', () => {
- // using the center position of the draggable as the selection point
+ // using the center position of the draggable as the selection point
const page: Position = draggable1.page.withMargin.center;
const withinDroppable: WithinDroppable = {
// just over the top of the second item
@@ -574,6 +576,42 @@ describe('get drag impact', () => {
});
});
});
+
+ describe('moving over disabled list', () => {
+ it('should return an empty impact', () => {
+ // moving forward past the top of the next item
+ const page: Position = {
+ x: draggable1.page.withoutMargin.center.x,
+ y: draggable2.page.withoutMargin.top + 1,
+ };
+ const withinDroppable: WithinDroppable = {
+ center: page,
+ };
+ // $ExpectError - using spread
+ const disabled: DroppableDimension = {
+ ...droppable,
+ isEnabled: false,
+ };
+ const custom: DroppableDimensionMap = {
+ [disabled.id]: disabled,
+ };
+ const expected: DragImpact = {
+ movement: noMovement,
+ direction: droppable.axis.direction,
+ destination: null,
+ };
+
+ const impact: DragImpact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId: draggable1.id,
+ draggables,
+ droppables: custom,
+ });
+
+ expect(impact).toEqual(expected);
+ });
+ });
});
// same tests as vertical - but moving on the horizontal plane
@@ -789,8 +827,8 @@ describe('get drag impact', () => {
expect(impact.movement.amount).toEqual(expected);
});
- it('should return the items that need to be moved', () => {
- expect(impact.movement.draggables).toEqual([draggable2.id, draggable3.id]);
+ it('should return the items that need to be moved (sorted by closest impacted)', () => {
+ expect(impact.movement.draggables).toEqual([draggable3.id, draggable2.id]);
});
});
@@ -1024,7 +1062,7 @@ describe('get drag impact', () => {
expect(impact.movement.amount).toEqual(expected);
});
- it('should return the items that need to be moved', () => {
+ it('should return the items that need to be moved (sorted by closest to the draggables current position)', () => {
expect(impact.movement.draggables).toEqual([draggable1.id, draggable2.id]);
});
});
@@ -1130,5 +1168,249 @@ describe('get drag impact', () => {
});
});
});
+
+ describe('moving over disabled list', () => {
+ it('should return an empty impact', () => {
+ // moving forward past the right of the next item
+ const page: Position = {
+ x: draggable2.page.withoutMargin.left + 1,
+ y: draggable1.page.withoutMargin.center.y,
+ };
+ const withinDroppable: WithinDroppable = {
+ center: page,
+ };
+ // $ExpectError - using flow
+ const disabled: DroppableDimension = {
+ ...droppable,
+ isEnabled: false,
+ };
+ const custom: DroppableDimensionMap = {
+ [disabled.id]: disabled,
+ };
+ const expected: DragImpact = {
+ movement: noMovement,
+ direction: droppable.axis.direction,
+ destination: null,
+ };
+
+ const impact: DragImpact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId: draggable1.id,
+ draggables,
+ droppables: custom,
+ });
+
+ expect(impact).toEqual(expected);
+ });
+ });
+ });
+
+ describe('moving between lists', () => {
+ const homeDroppable = getDroppableWithDraggables({
+ droppableId: 'drop-home',
+ droppableRect: { top: 0, left: 0, bottom: 600, right: 100 },
+ draggableRects: [
+ { top: 0, left: 0, bottom: 100, right: 100 },
+ { top: 101, left: 0, bottom: 300, right: 100 },
+ { top: 301, left: 0, bottom: 600, right: 100 },
+ ],
+ });
+
+ const destinationDroppable = getDroppableWithDraggables({
+ droppableId: 'drop-destination',
+ droppableRect: { top: 100, left: 110, bottom: 800, right: 210 },
+ draggableRects: [
+ { top: 100, left: 110, bottom: 400, right: 210 },
+ { top: 401, left: 110, bottom: 600, right: 210 },
+ { top: 601, left: 110, bottom: 700, right: 210 },
+ ],
+ });
+
+ const droppables = {
+ [homeDroppable.droppableId]: homeDroppable.droppable,
+ [destinationDroppable.droppableId]: destinationDroppable.droppable,
+ };
+
+ const draggables = {
+ ...homeDroppable.draggables,
+ ...destinationDroppable.draggables,
+ };
+
+ const draggableId = homeDroppable.draggableIds[0];
+ const draggedItem = homeDroppable.draggables[draggableId];
+
+ describe('moving outside a droppable', () => {
+ const page = {
+ x: homeDroppable.droppable.page.withMargin.center.x,
+ y: homeDroppable.droppable.page.withMargin.height + 1,
+ };
+ const withinDroppable = { center: page };
+ const impact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId,
+ draggables,
+ droppables,
+ });
+
+ it('should not return a destination', () => {
+ expect(impact.destination).toBe(null);
+ });
+ it('should not return a movement amount', () => {
+ expect(impact.movement.amount).toEqual(origin);
+ });
+ it('should not displace any items', () => {
+ expect(impact.movement.draggables.length).toBe(0);
+ });
+ });
+
+ describe('moving to the start of a foreign droppable', () => {
+ const page = {
+ x: destinationDroppable.droppable.page.withMargin.center.x,
+ y: destinationDroppable.droppable.page.withMargin.top + 1,
+ };
+ const withinDroppable = { center: page };
+ const impact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId,
+ draggables,
+ droppables,
+ });
+
+ it('should return the destination droppable', () => {
+ expect(impact.destination && impact.destination.droppableId)
+ .toBe(destinationDroppable.droppableId);
+ });
+ it('should return an index of 0 (first position)', () => {
+ expect(impact.destination && impact.destination.index).toEqual(0);
+ });
+ it('should indicate that items must be displaced forwards', () => {
+ expect(impact.movement.isBeyondStartPosition).toBe(false);
+ });
+ it('should indicate that items need to be displaced by the height of the dragged item', () => {
+ const expected = patch('y', draggedItem.page.withMargin.height);
+ expect(impact.movement.amount).toEqual(expected);
+ });
+ it('should displace all items in the destination droppable', () => {
+ expect(impact.movement.draggables).toEqual(destinationDroppable.draggableIds);
+ });
+ });
+
+ describe('moving to the second position of a foreign droppable', () => {
+ const page = {
+ x: destinationDroppable.droppable.page.withMargin.center.x,
+ y: destinationDroppable.draggables[
+ destinationDroppable.draggableIds[1]
+ ].page.withMargin.top + 1,
+ };
+ const withinDroppable = { center: page };
+ const impact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId,
+ draggables,
+ droppables,
+ });
+
+ it('should return the destination droppable', () => {
+ expect(impact.destination && impact.destination.droppableId)
+ .toBe(destinationDroppable.droppableId);
+ });
+ it('should return an index of 1 (second position)', () => {
+ expect(impact.destination && impact.destination.index).toEqual(1);
+ });
+ it('should indicate that items must be displaced forwards', () => {
+ expect(impact.movement.isBeyondStartPosition).toBe(false);
+ });
+ it('should indicate that items need to be displaced by the height of the dragged item', () => {
+ const expected = patch('y', draggedItem.page.withMargin.height);
+ expect(impact.movement.amount).toEqual(expected);
+ });
+ it('should displace all items in the destination droppable except the first', () => {
+ expect(impact.movement.draggables).toEqual(
+ destinationDroppable.draggableIds.slice(1 - destinationDroppable.draggableIds.length)
+ );
+ });
+ });
+
+ describe('moving to the end of a foreign droppable', () => {
+ const page = {
+ x: destinationDroppable.droppable.page.withMargin.center.x,
+ y: destinationDroppable.droppable.page.withMargin.bottom - 1,
+ };
+ const withinDroppable = { center: page };
+ const impact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId,
+ draggables,
+ droppables,
+ });
+
+ it('should return the destination droppable', () => {
+ expect(impact.destination && impact.destination.droppableId)
+ .toBe(destinationDroppable.droppableId);
+ });
+ it('should return an index equal to the number of draggables in the destination droppable', () => {
+ expect(impact.destination && impact.destination.index)
+ .toEqual(destinationDroppable.draggableIds.length);
+ });
+ it('should indicate that items must be displaced forwards', () => {
+ expect(impact.movement.isBeyondStartPosition).toBe(false);
+ });
+ it('should indicate that items need to be displaced by the height of the dragged item', () => {
+ const expected = patch('y', draggedItem.page.withMargin.height);
+ expect(impact.movement.amount).toEqual(expected);
+ });
+ it('should not displace any items', () => {
+ expect(impact.movement.draggables.length).toBe(0);
+ });
+ });
+
+ describe('when the foreign droppable is scrolled', () => {
+ // top of the first item
+ const page = {
+ x: destinationDroppable.droppable.page.withMargin.center.x,
+ y: destinationDroppable.droppable.page.withMargin.top + 1,
+ };
+
+ // scroll past the first item
+ const center = add(page, {
+ x: 0,
+ y: destinationDroppable.draggables[
+ destinationDroppable.draggableIds[0]
+ ].page.withMargin.height,
+ });
+ const withinDroppable = { center };
+ const impact = getDragImpact({
+ page,
+ withinDroppable,
+ draggableId,
+ draggables,
+ droppables,
+ });
+
+ it('should return the destination droppable', () => {
+ expect(impact.destination && impact.destination.droppableId)
+ .toBe(destinationDroppable.droppableId);
+ });
+ it('should account for scrolling when calculating the index', () => {
+ expect(impact.destination && impact.destination.index).toEqual(1);
+ });
+ it('should indicate that items must be displaced forwards', () => {
+ expect(impact.movement.isBeyondStartPosition).toBe(false);
+ });
+ it('should indicate that items need to be displaced by the height of the dragged item', () => {
+ const expected = patch('y', draggedItem.page.withMargin.height);
+ expect(impact.movement.amount).toEqual(expected);
+ });
+ it('should account for scrolling when determining which items are being displaced', () => {
+ expect(impact.movement.draggables).toEqual(
+ destinationDroppable.draggableIds.slice(1 - destinationDroppable.draggableIds.length)
+ );
+ });
+ });
});
});
diff --git a/test/unit/state/get-draggables-inside-droppable.spec.js b/test/unit/state/get-draggables-inside-droppable.spec.js
index f4b95d1101..27e5ea840f 100644
--- a/test/unit/state/get-draggables-inside-droppable.spec.js
+++ b/test/unit/state/get-draggables-inside-droppable.spec.js
@@ -1,7 +1,7 @@
// @flow
import getDraggablesInsideDroppable from '../../../src/state/get-draggables-inside-droppable';
import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import type {
DroppableId,
DraggableDimension,
@@ -94,6 +94,4 @@ describe('get draggables inside a droppable', () => {
expect(result).toEqual([inside1, inside2, inside3]);
});
-
- // other edge cases tested in get-inside-dimension
});
diff --git a/test/unit/state/get-droppable-over.spec.js b/test/unit/state/get-droppable-over.spec.js
index eecd40c29c..13703c7a0c 100644
--- a/test/unit/state/get-droppable-over.spec.js
+++ b/test/unit/state/get-droppable-over.spec.js
@@ -1,7 +1,7 @@
// @flow
import getDroppableOver from '../../../src/state/get-droppable-over';
import { getDroppableDimension } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import type {
DroppableDimension,
DroppableDimensionMap,
diff --git a/test/unit/state/get-initial-impact.spec.js b/test/unit/state/get-initial-impact.spec.js
new file mode 100644
index 0000000000..77cdca666b
--- /dev/null
+++ b/test/unit/state/get-initial-impact.spec.js
@@ -0,0 +1,76 @@
+// @flow
+import getInitialImpact from '../../../src/state/get-initial-impact';
+import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables';
+import { getDraggableDimension } from '../../../src/state/dimension';
+import getClientRect from '../../../src/state/get-client-rect';
+import { noMovement } from '../../../src/state/no-impact';
+import type { Result as Data } from '../../utils/get-droppable-with-draggables';
+import type {
+ DraggableDimensionMap,
+ DraggableDimension,
+ DroppableDimension,
+ DragImpact,
+} from '../../../src/types';
+
+const data: Data = getDroppableWithDraggables({
+ droppableId: 'droppable',
+ droppableRect: { top: 0, left: 0, right: 100, bottom: 100 },
+ draggableRects: [
+ { top: 0, left: 0, right: 100, bottom: 20 },
+ { top: 20, left: 0, right: 100, bottom: 40 },
+ { top: 40, left: 0, right: 100, bottom: 60 },
+ { top: 60, left: 0, right: 100, bottom: 80 },
+ ],
+});
+
+const droppable: DroppableDimension = data.droppable;
+const draggables: DraggableDimensionMap = data.draggables;
+
+describe('get initial impact', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ console.error.mockRestore();
+ });
+
+ it('should return null if the draggable cannot be found in the droppable', () => {
+ const mystery = getDraggableDimension({
+ id: 'mystery',
+ droppableId: 'some-cool-id',
+ clientRect: getClientRect({
+ top: 0, left: 100, right: 200, bottom: 20,
+ }),
+ });
+
+ const result: ?DragImpact = getInitialImpact({
+ draggable: mystery,
+ droppable: data.droppable,
+ draggables: data.draggables,
+ });
+
+ expect(result).toBe(null);
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should return the initial location', () => {
+ data.draggableDimensions.forEach((draggable: DraggableDimension, index: number) => {
+ const expected: DragImpact = {
+ movement: noMovement,
+ direction: droppable.axis.direction,
+ destination: {
+ droppableId: droppable.id,
+ index,
+ },
+ };
+ const impact: ?DragImpact = getInitialImpact({
+ draggable,
+ droppable,
+ draggables,
+ });
+
+ expect(impact).toEqual(expected);
+ });
+ });
+});
diff --git a/test/unit/state/get-new-home-client-center.spec.js b/test/unit/state/get-new-home-client-center.spec.js
new file mode 100644
index 0000000000..38873882b6
--- /dev/null
+++ b/test/unit/state/get-new-home-client-center.spec.js
@@ -0,0 +1,301 @@
+// @flow
+import getNewHomeClientCenter from '../../../src/state/get-new-home-client-center';
+import { noMovement } from '../../../src/state/no-impact';
+import { patch } from '../../../src/state/position';
+import { getDroppableDimension, getDraggableDimension } from '../../../src/state/dimension';
+import { vertical, horizontal } from '../../../src/state/axis';
+import getClientRect from '../../../src/state/get-client-rect';
+import moveToEdge from '../../../src/state/move-to-edge';
+import type {
+ Axis,
+ DragMovement,
+ Position,
+ DraggableDimension,
+ DroppableDimension,
+ DraggableDimensionMap,
+} from '../../../src/types';
+
+describe('get new home client center', () => {
+ [vertical, horizontal].forEach((axis: Axis) => {
+ describe(`dropping on ${axis.direction} list`, () => {
+ const crossAxisStart: number = 0;
+ const crossAxisEnd: number = 100;
+
+ const home: DroppableDimension = getDroppableDimension({
+ id: 'home',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.end]: 100,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ }),
+ });
+
+ // size 10
+ const inHome1: DraggableDimension = getDraggableDimension({
+ id: 'inHome1',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.end]: 10,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ }),
+ });
+
+ // size 20
+ const inHome2: DraggableDimension = getDraggableDimension({
+ id: 'inHome2',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ [axis.start]: 10,
+ [axis.end]: 30,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ }),
+ });
+
+ // size 30
+ const inHome3: DraggableDimension = getDraggableDimension({
+ id: 'inHome3',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ [axis.start]: 30,
+ [axis.end]: 60,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ }),
+ });
+
+ const draggables: DraggableDimensionMap = {
+ [inHome1.id]: inHome1,
+ [inHome2.id]: inHome2,
+ [inHome3.id]: inHome3,
+ };
+
+ const inHome1Size: Position = patch(axis.line, inHome1.page.withMargin[axis.size]);
+
+ it('should return the original center dropped on no destination', () => {
+ const result: Position = getNewHomeClientCenter({
+ movement: noMovement,
+ draggables,
+ draggable: inHome1,
+ destination: null,
+ });
+
+ expect(result).toEqual(inHome1.client.withMargin.center);
+ });
+
+ describe('dropping in home list', () => {
+ it('should return the original center if moving back into the same spot', () => {
+ const newCenter: Position = getNewHomeClientCenter({
+ movement: noMovement,
+ draggables,
+ draggable: inHome1,
+ destination: home,
+ });
+
+ expect(newCenter).toEqual(inHome1.client.withMargin.center);
+ });
+
+ describe('is moving forward (is always beyond start position)', () => {
+ // moving the first item forward past the third item
+ it('should move after the closest impacted draggable', () => {
+ const targetCenter: Position = moveToEdge({
+ source: inHome1.client.withMargin,
+ sourceEdge: 'end',
+ destination: inHome3.client.withMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+ // the movement from the last drag
+ const movement: DragMovement = {
+ // ordered by closest to impacted
+ draggables: [inHome3.id, inHome2.id],
+ amount: inHome1Size,
+ isBeyondStartPosition: true,
+ };
+
+ const newCenter = getNewHomeClientCenter({
+ movement,
+ draggables,
+ draggable: inHome1,
+ destination: home,
+ });
+
+ expect(newCenter).toEqual(targetCenter);
+ });
+ });
+
+ describe('is moving backward (is always not beyond start position)', () => {
+ // moving inHome3 back past inHome1
+ it('should move before the closest impacted draggable', () => {
+ const targetCenter: Position = moveToEdge({
+ source: inHome3.client.withMargin,
+ sourceEdge: 'start',
+ destination: inHome1.client.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+ // the movement from the last drag
+ const movement: DragMovement = {
+ // ordered by closest to impacted
+ draggables: [inHome1.id, inHome2.id],
+ amount: inHome1Size,
+ // is not beyond start position - going backwards
+ isBeyondStartPosition: false,
+ };
+
+ const newCenter = getNewHomeClientCenter({
+ movement,
+ draggables,
+ draggable: inHome3,
+ destination: home,
+ });
+
+ expect(newCenter).toEqual(targetCenter);
+ });
+ });
+ });
+
+ describe('dropping in foreign list', () => {
+ const foreignCrossAxisStart: number = 100;
+ const foreignCrossAxisEnd: number = 200;
+ const foreign: DroppableDimension = getDroppableDimension({
+ id: 'foreign',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.end]: 100,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ }),
+ });
+
+ // size 10
+ const inForeign1: DraggableDimension = getDraggableDimension({
+ id: 'inForeign1',
+ droppableId: foreign.id,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.end]: 10,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ }),
+ });
+ // size 20
+ const inForeign2: DraggableDimension = getDraggableDimension({
+ id: 'inForeign2',
+ droppableId: foreign.id,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.end]: 10,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ }),
+ });
+
+ const withForeign: DraggableDimensionMap = {
+ ...draggables,
+ [inForeign1.id]: inForeign1,
+ [inForeign2.id]: inForeign2,
+ };
+
+ describe('is moving into a populated list', () => {
+ it('should move above the target', () => {
+ const targetCenter: Position = moveToEdge({
+ source: inHome1.client.withMargin,
+ sourceEdge: 'start',
+ destination: inForeign1.client.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+ // the movement from the last drag
+ const movement: DragMovement = {
+ // ordered by closest to impacted
+ draggables: [inForeign1.id, inForeign2.id],
+ amount: inHome1Size,
+ // not relevant when moving into new list
+ isBeyondStartPosition: false,
+ };
+
+ const newCenter = getNewHomeClientCenter({
+ movement,
+ draggables: withForeign,
+ draggable: inHome1,
+ destination: foreign,
+ });
+
+ expect(newCenter).toEqual(targetCenter);
+ });
+ });
+
+ describe('is moving to end of a list', () => {
+ it('should draggable below the last item in the list', () => {
+ const targetCenter: Position = moveToEdge({
+ source: inHome1.client.withMargin,
+ sourceEdge: 'start',
+ // will target the last in the foreign droppable
+ destination: inForeign2.client.withMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+ // the movement from the last drag
+ const movement: DragMovement = {
+ // nothing has moved (going to end of list)
+ draggables: [],
+ amount: inHome1Size,
+ // not relevant when moving into new list
+ isBeyondStartPosition: false,
+ };
+
+ const newCenter = getNewHomeClientCenter({
+ movement,
+ draggables: withForeign,
+ draggable: inHome1,
+ destination: foreign,
+ });
+
+ expect(newCenter).toEqual(targetCenter);
+ });
+ });
+
+ describe('is moving to empty list', () => {
+ it('should move to the start of the list', () => {
+ const empty: DroppableDimension = getDroppableDimension({
+ id: 'empty',
+ clientRect: getClientRect({
+ top: 1000, bottom: 2000, left: 1000, right: 1000,
+ }),
+ });
+
+ const targetCenter: Position = moveToEdge({
+ source: inHome1.client.withMargin,
+ sourceEdge: 'start',
+ destination: empty.client.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+ // the movement from the last drag
+ const movement: DragMovement = {
+ draggables: [],
+ amount: inHome1Size,
+ // not relevant when moving into new list
+ isBeyondStartPosition: false,
+ };
+
+ const newCenter = getNewHomeClientCenter({
+ movement,
+ draggables: withForeign,
+ draggable: inHome1,
+ destination: empty,
+ });
+
+ expect(newCenter).toEqual(targetCenter);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js
deleted file mode 100644
index 0bb2c2a67f..0000000000
--- a/test/unit/state/get-new-home-client-offset.spec.js
+++ /dev/null
@@ -1,761 +0,0 @@
-// @flow
-import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset';
-import noImpact from '../../../src/state/no-impact';
-import { getDraggableDimension } from '../../../src/state/dimension';
-import { add, subtract } from '../../../src/state/position';
-import getClientRect from '../../utils/get-client-rect';
-import { vertical, horizontal } from '../../../src/state/axis';
-import type {
- DroppableId,
- DragMovement,
- Position,
- DraggableDimension,
- DraggableDimensionMap,
-} from '../../../src/types';
-
-const origin: Position = { x: 0, y: 0 };
-const droppableId: DroppableId = 'drop-1';
-
-describe('get new home client offset', () => {
- beforeEach(() => {
- jest.spyOn(console, 'error').mockImplementation(() => { });
- });
-
- afterEach(() => {
- console.error.mockRestore();
- });
-
- describe('vertical', () => {
- const draggable1: DraggableDimension = getDraggableDimension({
- id: 'drag-1',
- droppableId,
- clientRect: getClientRect({
- top: 0,
- left: 0,
- bottom: 100,
- right: 100,
- }),
- });
-
- // huge height: 199
- const draggable2: DraggableDimension = getDraggableDimension({
- id: 'drag-2',
- droppableId,
- clientRect: getClientRect({
- top: 101,
- left: 0,
- bottom: 300,
- right: 100,
- }),
- });
-
- // height: 299
- const draggable3: DraggableDimension = getDraggableDimension({
- id: 'drag-3',
- droppableId,
- clientRect: getClientRect({
- top: 301,
- left: 0,
- bottom: 600,
- right: 100,
- }),
- });
-
- const draggables: DraggableDimensionMap = {
- [draggable1.id]: draggable1,
- [draggable2.id]: draggable2,
- [draggable3.id]: draggable3,
- };
-
- it('should return the total scroll diff if nothing has moved', () => {
- const offset: Position = {
- x: 100,
- y: 200,
- };
- const droppableScrollDiff: Position = {
- x: 20,
- y: 10,
- };
- const windowScrollDiff: Position = {
- x: 30,
- y: 20,
- };
-
- const result: Position = getNewHomeClientOffset({
- movement: noImpact.movement,
- clientOffset: offset,
- pageOffset: offset,
- droppableScrollDiff,
- windowScrollDiff,
- draggables,
- axis: vertical,
- });
-
- expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff));
- });
-
- it('should return the total scroll diff is no axis is provided', () => {
- const offset: Position = {
- x: 100,
- y: 200,
- };
- const droppableScrollDiff: Position = {
- x: 20,
- y: 10,
- };
- const windowScrollDiff: Position = {
- x: 30,
- y: 20,
- };
- // There should be no movement without an axis
- // This is an error situation
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: 0,
- y: draggable1.page.withMargin.height,
- },
- isBeyondStartPosition: true,
- };
-
- const result: Position = getNewHomeClientOffset({
- movement,
- clientOffset: offset,
- pageOffset: offset,
- droppableScrollDiff,
- windowScrollDiff,
- draggables,
- axis: null,
- });
-
- expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff));
- // this is an error situation
- expect(console.error).toHaveBeenCalled();
- });
-
- describe('moving forward', () => {
- // Moving the first item down into the third position
-
- // Where the users cursor is
- const selection: Position = {
- x: draggable1.client.withoutMargin.center.x,
- y: draggable3.client.withoutMargin.top + 1,
- };
-
- // moving the first item down past the third item
- it('should account for the current client location of the dragging item', () => {
- // The offset needed to get to the selection.
- const clientOffset: Position = subtract(selection, draggable1.client.withoutMargin.center);
-
- // this test does not exercise page movement
- const pageOffset: Position = clientOffset;
-
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: 0,
- y: draggable1.page.withMargin.height,
- },
- isBeyondStartPosition: true,
- };
-
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const verticalChange = {
- x: 0,
- y: draggable2.page.withMargin.height + draggable3.page.withMargin.height,
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the first item down past the third using only container scroll
- it('should account for any changes in the droppables scroll container', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = origin;
- const droppableScrollDiff: Position = subtract(
- selection,
- draggable1.page.withoutMargin.center
- );
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: 0,
- y: draggable1.page.withMargin.height,
- },
- isBeyondStartPosition: true,
- };
- // this is where it needs to end up
- const verticalChange = {
- x: 0,
- y: draggable2.page.withMargin.height + draggable3.page.withMargin.height,
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff,
- windowScrollDiff: origin,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the first item down past the third using only window scroll
- it('should account for any changes in the window scroll', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = {
- x: 10,
- y: 200,
- };
- const droppableScrollDiff = origin;
- const windowScrollDiff = pageOffset;
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: 0,
- y: draggable1.page.withMargin.height,
- },
- isBeyondStartPosition: true,
- };
- // this is where it needs to end up
- const verticalChange = {
- x: 0,
- y: draggable2.page.withMargin.height + draggable3.page.withMargin.height,
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
- });
-
- describe('moving backward', () => {
- // Moving the third item back into the first position
-
- // Where the users cursor is
- const selection: Position = {
- x: draggable3.client.withoutMargin.center.x,
- y: draggable1.client.withoutMargin.bottom - 1,
- };
-
- // moving the third item backwards past the first and second item
- it('should account for the current client location of the dragging item', () => {
- // The offset needed to get to the selection.
- const clientOffset: Position = subtract(selection, draggable3.client.withoutMargin.center);
-
- // this test does not exercise page movement
- const pageOffset: Position = clientOffset;
-
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: 0,
- y: draggable3.page.withMargin.height,
- },
- isBeyondStartPosition: false,
- };
-
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const verticalChange = {
- x: 0,
- y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height),
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the third item back past the first and second item using only window scroll
- it('should account for the current page location of the dragging item', () => {
- // have not moved the item on the screen at all
- const clientOffset: Position = origin;
- // the window has scrolled to get it to the selection point
- const pageOffset: Position = subtract(selection, draggable3.page.withoutMargin.center);
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: 0,
- y: draggable3.page.withMargin.height,
- },
- isBeyondStartPosition: false,
- };
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const verticalChange = {
- x: 0,
- y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height),
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the third item backwards past the first and second item using only container scroll
- it('should account for any changes in the droppables scroll container', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = origin;
- const droppableScrollDiff: Position = subtract(
- selection,
- draggable3.page.withoutMargin.center
- );
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: 0,
- y: draggable3.page.withMargin.height,
- },
- isBeyondStartPosition: false,
- };
- // this is where it needs to end up
- const verticalChange = {
- x: 0,
- y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height),
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(verticalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff,
- windowScrollDiff: origin,
- draggables,
- axis: vertical,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
- });
- });
-
- describe('horizontal', () => {
- const draggable1: DraggableDimension = getDraggableDimension({
- id: 'drag-1',
- droppableId,
- clientRect: getClientRect({
- top: 0,
- left: 0,
- bottom: 100,
- right: 100,
- }),
- });
-
- // width: 199
- const draggable2: DraggableDimension = getDraggableDimension({
- id: 'drag-2',
- droppableId,
- clientRect: getClientRect({
- top: 0,
- left: 101,
- bottom: 100,
- right: 300,
- }),
- });
-
- // height: 299
- const draggable3: DraggableDimension = getDraggableDimension({
- id: 'drag-3',
- droppableId,
- clientRect: getClientRect({
- top: 0,
- left: 500,
- bottom: 100,
- right: 301,
- }),
- });
-
- const draggables: DraggableDimensionMap = {
- [draggable1.id]: draggable1,
- [draggable2.id]: draggable2,
- [draggable3.id]: draggable3,
- };
-
- it('should return to the total scroll diff if nothing has moved', () => {
- const offset: Position = {
- x: 100,
- y: 200,
- };
- const droppableScrollDiff: Position = {
- x: 20,
- y: 10,
- };
- const windowScrollDiff: Position = {
- x: 30,
- y: 20,
- };
-
- const result: Position = getNewHomeClientOffset({
- movement: noImpact.movement,
- clientOffset: offset,
- pageOffset: offset,
- droppableScrollDiff,
- windowScrollDiff,
- draggables,
- axis: horizontal,
- });
-
- expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff));
- });
-
- it('should return the total scroll diff is no axis is provided', () => {
- const offset: Position = {
- x: 100,
- y: 200,
- };
- const droppableScrollDiff: Position = {
- x: 20,
- y: 10,
- };
- const windowScrollDiff: Position = {
- x: 30,
- y: 20,
- };
- // There should be no movement without an axis
- // This is an error situation
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: draggable1.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: true,
- };
-
- const result: Position = getNewHomeClientOffset({
- movement,
- clientOffset: offset,
- pageOffset: offset,
- droppableScrollDiff,
- windowScrollDiff,
- draggables,
- axis: null,
- });
-
- expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff));
- // this is an error situation
- expect(console.error).toHaveBeenCalled();
- });
-
- describe('moving forward', () => {
- // Moving the first item forward into the third position
-
- // Where the users cursor is
- const selection: Position = {
- x: draggable3.client.withoutMargin.left + 1,
- y: draggable1.client.withoutMargin.center.y,
- };
-
- // moving the first item down past the third item
- it('should account for the current client location of the dragging item', () => {
- // The offset needed to get to the selection.
- const clientOffset: Position = subtract(selection, draggable1.client.withoutMargin.center);
-
- // this test does not exercise page movement
- const pageOffset: Position = clientOffset;
-
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: draggable1.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: true,
- };
-
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const horizontalChange = {
- x: draggable2.page.withMargin.width + draggable3.page.withMargin.width,
- y: 0,
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the first item down past the third using only container scroll
- it('should account for any changes in the droppables scroll container', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = origin;
- const droppableScrollDiff: Position = subtract(
- selection,
- draggable1.page.withoutMargin.center
- );
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: draggable1.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: true,
- };
- // this is where it needs to end up
- const horizontalChange = {
- x: draggable2.page.withMargin.width + draggable3.page.withMargin.width,
- y: 0,
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff,
- windowScrollDiff: origin,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the first item forward past the third using only window scroll
- it('should account for any changes in the window scroll', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = {
- x: 10,
- y: 200,
- };
- const droppableScrollDiff = origin;
- const windowScrollDiff = pageOffset;
- const movement: DragMovement = {
- draggables: [draggable2.id, draggable3.id],
- amount: {
- x: draggable1.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: true,
- };
- // this is where it needs to end up
- const horizontalChange = {
- x: draggable2.page.withMargin.width + draggable3.page.withMargin.width,
- y: 0,
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
- });
-
- describe('moving backward', () => {
- // Moving the third item back into the first position
-
- // Where the users cursor is
- const selection: Position = {
- x: draggable1.client.withoutMargin.right - 1,
- y: draggable3.client.withoutMargin.center.y,
- };
-
- // moving the third item back past the first and second item
- it('should account for the current client location of the dragging item', () => {
- // The offset needed to get to the selection.
- const clientOffset: Position = subtract(selection, draggable3.client.withoutMargin.center);
-
- // this test does not exercise page movement
- const pageOffset: Position = clientOffset;
-
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: draggable3.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: false,
- };
-
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const horizontalChange = {
- x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width),
- y: 0,
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the third item back past the first and second item using only window scroll
- it('should account for the current page location of the dragging item', () => {
- // have not moved the item on the screen at all
- const clientOffset: Position = origin;
- // the window has scrolled to get it to the selection point
- const pageOffset: Position = subtract(selection, draggable3.page.withoutMargin.center);
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: draggable3.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: false,
- };
- // How much distance the item needs to travel to be in its new home
- // from where it started
- const horizontalChange = {
- x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width),
- y: 0,
- };
- // How far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(clientOffset, diff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff: origin,
- windowScrollDiff: origin,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
-
- // moving the first item down past the third using only container scroll
- it('should account for any changes in the droppables scroll container', () => {
- const clientOffset: Position = origin;
- const pageOffset: Position = origin;
- const droppableScrollDiff: Position = subtract(
- selection,
- draggable3.page.withoutMargin.center
- );
- const movement: DragMovement = {
- draggables: [draggable1.id, draggable2.id],
- amount: {
- x: draggable3.page.withMargin.width,
- y: 0,
- },
- isBeyondStartPosition: false,
- };
- // this is where it needs to end up
- const horizontalChange = {
- x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width),
- y: 0,
- };
- // this is how far away it is from where it needs to end up
- const diff: Position = subtract(horizontalChange, pageOffset);
- // this is the final client offset
- const expected = add(diff, droppableScrollDiff);
-
- const newHomeOffset = getNewHomeClientOffset({
- movement,
- clientOffset,
- pageOffset,
- droppableScrollDiff,
- windowScrollDiff: origin,
- draggables,
- axis: horizontal,
- });
-
- expect(newHomeOffset).toEqual(expected);
- });
- });
- });
-});
diff --git a/test/unit/state/hook-middleware.spec.js b/test/unit/state/hook-middleware.spec.js
index a04dc4e2d8..42a600d1d0 100644
--- a/test/unit/state/hook-middleware.spec.js
+++ b/test/unit/state/hook-middleware.spec.js
@@ -2,7 +2,7 @@
import middleware from '../../../src/state/hook-middleware';
import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension';
import { clean } from '../../../src/state/action-creators';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import noImpact from '../../../src/state/no-impact';
import type {
DraggableId,
@@ -124,6 +124,7 @@ const state = (() => {
center: currentClient.center,
},
shouldAnimate: true,
+ isScrollAllowed: true,
},
impact: noImpact,
};
diff --git a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js
new file mode 100644
index 0000000000..43826498e8
--- /dev/null
+++ b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js
@@ -0,0 +1,132 @@
+// @flow
+import { isPointWithin, isDraggableWithin } from '../../../src/state/is-within-visible-bounds-of-droppable';
+import { getDroppableDimension } from '../../../src/state/dimension';
+import { add, subtract } from '../../../src/state/position';
+import getClientRect from '../../../src/state/get-client-rect';
+import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables';
+import type { Result } from '../../utils/get-droppable-with-draggables';
+import type {
+ Position,
+ DraggableDimension,
+} from '../../../src/types';
+
+describe('is within visible bounds of a droppable', () => {
+ const droppable = getDroppableDimension({
+ id: 'droppable',
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ });
+
+ describe('is point within', () => {
+ const isWithinDroppable = isPointWithin(droppable);
+ const { top, left, right, bottom } = droppable.page.withMargin;
+
+ it('should return true if a point is within a droppable', () => {
+ expect(isWithinDroppable(droppable.page.withMargin.center)).toBe(true);
+ });
+
+ it('should return true if a point is on any of the droppable boundaries', () => {
+ const corners = [
+ { x: left, y: top },
+ { x: left, y: bottom },
+ { x: right, y: top },
+ { x: right, y: bottom },
+ ];
+
+ corners.forEach((corner: Position) => {
+ expect(isWithinDroppable(corner)).toBe(true);
+ });
+ });
+
+ it('should return false if the point is not within the droppable on any side', () => {
+ const outside = [
+ subtract({ x: left, y: top }, { x: 0, y: 10 }), // too far top
+ subtract({ x: left, y: bottom }, { x: 10, y: 0 }), // too far left
+ add({ x: right, y: top }, { x: 10, y: 0 }), // too far right
+ add({ x: right, y: bottom }, { x: 0, y: 10 }), // too far bottom
+ ];
+
+ outside.forEach((point: Position) => {
+ expect(isWithinDroppable(point)).toBe(false);
+ });
+ });
+
+ it('should be based on the page coordinates of the droppable', () => {
+ const windowScroll: Position = {
+ x: 200, y: 200,
+ };
+ const custom = getDroppableDimension({
+ id: 'with-scroll',
+ windowScroll,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ });
+ const isWithinCustom = isPointWithin(custom);
+
+ // custom points
+ expect(isWithinCustom({ x: 10, y: 10 })).toBe(false);
+ expect(isWithinCustom({ x: 210, y: 210 })).toBe(true);
+
+ // checking with the center position of the dimension itself
+ expect(isWithinCustom(custom.client.withMargin.center)).toBe(false);
+ expect(isWithinCustom(custom.page.withMargin.center)).toBe(true);
+ });
+ });
+
+ describe('is draggable within', () => {
+ it('should return true if the draggable is within the droppable', () => {
+ const result: Result = getDroppableWithDraggables({
+ droppableRect: { top: 0, left: 0, bottom: 100, right: 100 },
+ draggableRects: [
+ // on the boundaries
+ { top: 0, left: 0, bottom: 100, right: 100 },
+ ],
+ });
+ const isWithinDroppable = isDraggableWithin(result.droppable);
+
+ expect(isWithinDroppable(result.draggableDimensions[0])).toBe(true);
+ });
+
+ it('should return false if there is overlap on any side', () => {
+ const result: Result = getDroppableWithDraggables({
+ droppableRect: { top: 0, left: 0, bottom: 100, right: 100 },
+ draggableRects: [
+ { top: -10, left: 0, bottom: 20, right: 100 }, // too far top
+ { top: 0, left: -10, bottom: 20, right: 100 }, // too far left
+ { top: 0, left: 0, bottom: 20, right: 110 }, // too far right
+ { top: 0, left: 0, bottom: 120, right: 100 }, // too far bottom
+ ],
+ });
+ const isWithinDroppable = isDraggableWithin(result.droppable);
+
+ result.draggableDimensions.forEach((draggable: DraggableDimension) => {
+ expect(isWithinDroppable(draggable)).toBe(false);
+ });
+ });
+
+ it('should allow a small affordance to compensate for margin capturing inaccuracy', () => {
+ const result: Result = getDroppableWithDraggables({
+ droppableRect: { top: 0, left: 0, bottom: 100, right: 100 },
+ draggableRects: [
+ { top: -1, left: 0, bottom: 20, right: 100 }, // not too far top
+ { top: 0, left: -1, bottom: 20, right: 100 }, // not too far left
+ { top: 0, left: 0, bottom: 20, right: 101 }, // not too far right
+ { top: 0, left: 0, bottom: 101, right: 100 }, // not too far bottom
+ ],
+ });
+ const isWithinDroppable = isDraggableWithin(result.droppable);
+
+ result.draggableDimensions.forEach((draggable: DraggableDimension) => {
+ expect(isWithinDroppable(draggable)).toBe(true);
+ });
+ });
+ });
+});
diff --git a/test/unit/state/is-within.spec.js b/test/unit/state/is-within.spec.js
new file mode 100644
index 0000000000..679e4d8a46
--- /dev/null
+++ b/test/unit/state/is-within.spec.js
@@ -0,0 +1,28 @@
+// @flow
+import isWithin from '../../../src/state/is-within';
+
+describe('is within', () => {
+ const lowerBound: number = 5;
+ const upperBound: number = 10;
+ const execute = isWithin(5, 10);
+
+ it('should return true when the value is between the bounds', () => {
+ expect(execute(lowerBound + 1)).toBe(true);
+ });
+
+ it('should return true when the value is equal to the lower bound', () => {
+ expect(execute(lowerBound)).toBe(true);
+ });
+
+ it('should return true when the value is equal to the upper bound', () => {
+ expect(execute(upperBound)).toBe(true);
+ });
+
+ it('should return false when the value is less then the lower bound', () => {
+ expect(execute(lowerBound - 1)).toBe(false);
+ });
+
+ it('should return false when the value is greater than the upper bound', () => {
+ expect(execute(upperBound + 1)).toBe(false);
+ });
+});
diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js
deleted file mode 100644
index 08343d29c1..0000000000
--- a/test/unit/state/jump-to-next-index.spec.js
+++ /dev/null
@@ -1,639 +0,0 @@
-// @flow
-import jumpToNextIndex from '../../../src/state/jump-to-next-index';
-import type { JumpToNextResult } from '../../../src/state/jump-to-next-index';
-import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
-import { patch } from '../../../src/state/position';
-import { vertical, horizontal } from '../../../src/state/axis';
-import type {
- Axis,
- DragMovement,
- DragImpact,
- DroppableId,
- DraggableDimension,
- DroppableDimension,
- DraggableDimensionMap,
- DroppableDimensionMap,
- DraggableLocation,
- Position,
-} from '../../../src/types';
-
-const droppableId: DroppableId = 'drop-1';
-
-describe('jump to next index', () => {
- [vertical, horizontal].forEach((axis: Axis) => {
- const droppable: DroppableDimension = getDroppableDimension({
- id: droppableId,
- direction: axis.direction,
- clientRect: getClientRect({
- top: 0,
- left: 0,
- bottom: 1000,
- right: 1000,
- }),
- });
-
- // size: 100
- const draggable1: DraggableDimension = getDraggableDimension({
- id: 'draggable1',
- droppableId,
- clientRect: getClientRect({
- top: 0,
- left: 0,
- bottom: 100,
- right: 100,
- }),
- });
-
- // size: 199
- const draggable2: DraggableDimension = getDraggableDimension({
- id: 'draggable2',
- droppableId,
- clientRect: getClientRect({
- top: 101,
- left: 101,
- bottom: 300,
- right: 300,
- }),
- });
-
- // size: 299
- const draggable3: DraggableDimension = getDraggableDimension({
- id: 'draggable3',
- droppableId,
- clientRect: getClientRect({
- top: 301,
- left: 301,
- bottom: 600,
- right: 600,
- }),
- });
-
- const droppables: DroppableDimensionMap = {
- [droppable.id]: droppable,
- };
-
- const draggables: DraggableDimensionMap = {
- [draggable1.id]: draggable1,
- [draggable2.id]: draggable2,
- [draggable3.id]: draggable3,
- };
- describe(`on the ${axis.direction} axis`, () => {
- describe('jump forward', () => {
- it('should return null if cannot move forward', () => {
- const impact: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- destination: {
- index: 2,
- droppableId: droppable.id,
- },
- };
-
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable3.id,
- impact,
- draggables,
- droppables,
- });
-
- expect(result).toBe(null);
- });
-
- describe('is moving toward start position', () => {
- describe('dragging item forward to starting position', () => {
- // dragging the second item (draggable2), which has previously
- // been moved backwards and is now in the first position
- const impact: DragImpact = {
- movement: {
- draggables: [draggable1.id],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- destination: {
- index: 0,
- droppableId: droppable.id,
- },
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable2.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result of jumpToNextIndex');
- }
-
- it('should return the size of the dimension in the current index as the diff', () => {
- const size: Position = patch(
- axis.line,
- draggable1.page.withMargin[axis.size]
- );
-
- expect(result.diff).toEqual(size);
- });
-
- it('should return an empty impact', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- direction: axis.direction,
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
-
- describe('dragging forwards, but not beyond the starting position', () => {
- // draggable3 has moved backwards past draggable2 and draggable1
- const impact: DragImpact = {
- movement: {
- // second and first item have already moved
- draggables: [draggable2.id, draggable1.id],
- amount: patch(axis.line, draggable3.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- // draggable3 is now in the first position
- destination: {
- droppableId: droppable.id,
- index: 0,
- },
- };
- // moving draggable3 forward one position
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable3.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the moving dimension (draggable1)', () => {
- const expected: Position = patch(
- axis.line,
- draggable1.page.withMargin[axis.size],
- );
- expect(result.diff).toEqual(expected);
- });
-
- it('should remove the first dimension from the impact', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [draggable2.id],
- amount: patch(axis.line, draggable3.page.withMargin[axis.size]),
- // is still behind where it started
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- // is now in the second position
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
- });
-
- describe('is moving away from start position', () => {
- describe('dragging first item forward one position', () => {
- // dragging the first item forward into the second position
- const movement: DragMovement = {
- draggables: [],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- };
- const destination: DraggableLocation = {
- index: 0,
- droppableId: droppable.id,
- };
- const impact: DragImpact = {
- movement,
- direction: axis.direction,
- destination,
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable1.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the second dimension as the diff', () => {
- const expected: Position = patch(axis.line, draggable2.page.withMargin[axis.size]);
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should move the item into the second spot and move the second item out of the way', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [draggable2.id],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- // is now in the second position
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
-
- describe('dragging second item forward one position', () => {
- const movement: DragMovement = {
- draggables: [],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- };
- const destination: DraggableLocation = {
- index: 1,
- droppableId: droppable.id,
- };
- const impact: DragImpact = {
- movement,
- direction: axis.direction,
- destination,
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable2.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the third dimension as the diff', () => {
- const expected: Position = patch(
- axis.line,
- draggable3.page.withMargin[axis.size],
- );
- expect(result.diff).toEqual(expected);
- });
-
- it('should move the dragging item into the third spot and move the third item out of the way', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [draggable3.id],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- // is now in the second position
- destination: {
- droppableId: droppable.id,
- index: 2,
- },
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
-
- describe('dragging first item forward one position after already moving it forward once', () => {
- const impact: DragImpact = {
- movement: {
- // second item has already moved
- draggables: [draggable2.id],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- // draggable1 is now in the second position
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: true,
- draggableId: draggable1.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the third dimension', () => {
- // next dimension from the current index is draggable3
- const expected: Position = patch(
- axis.line,
- draggable3.page.withMargin[axis.size],
- );
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should move the third item out of the way', () => {
- const expected: DragImpact = {
- movement: {
- // adding draggable3 to the list
- draggables: [draggable2.id, draggable3.id],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- // is now in the second position
- destination: {
- droppableId: droppable.id,
- index: 2,
- },
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
- });
- });
-
- describe('jump backward', () => {
- it('should return null if cannot move backward', () => {
- const impact: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- destination: {
- index: 0,
- droppableId: droppable.id,
- },
- };
-
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: false,
- draggableId: draggable1.id,
- impact,
- draggables,
- droppables,
- });
-
- expect(result).toBe(null);
- });
-
- describe('is moving towards the start position', () => {
- describe('moving back to original position', () => {
- // dragged the second item (draggable2) forward once, and is now
- // moving backwards towards the start again
- const impact: DragImpact = {
- movement: {
- draggables: [draggable3.id],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- destination: {
- index: 2,
- droppableId: droppable.id,
- },
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: false,
- draggableId: draggable2.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the dimension that will move back to its home', () => {
- const expected: Position = patch(
- axis.line,
- // amount is negative because we are moving backwards
- -draggable3.page.withMargin[axis.size],
- );
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should return an empty impact', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- direction: axis.direction,
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
-
- describe('moving back, but not far enough to be at the start yet', () => {
- // dragged the first item:
- // forward twice so it is in the third position
- // then moving backward so it is in the second position
- const impact: DragImpact = {
- movement: {
- draggables: [draggable2.id, draggable3.id],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- destination: {
- index: 2,
- droppableId: droppable.id,
- },
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: false,
- draggableId: draggable1.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the size of the dimension that will move back to its home', () => {
- const expected: Position = patch(
- axis.line,
- // amount is negative because we are moving backwards
- -draggable3.page.withMargin[axis.size],
- );
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should remove the third draggable from the drag impact', () => {
- const expected: DragImpact = {
- movement: {
- // draggable3 has been removed
- draggables: [draggable2.id],
- amount: patch(axis.line, draggable1.page.withMargin[axis.size]),
- isBeyondStartPosition: true,
- },
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- direction: axis.direction,
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
- });
-
- describe('is moving away from start position', () => {
- describe('dragging the second item back to the first position', () => {
- // no impact yet
- const impact: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- index: 1,
- },
- direction: axis.direction,
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: false,
- draggableId: draggable2.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return negated size of draggable1 as the diff', () => {
- const expected: Position = patch(
- axis.line,
- -draggable1.page.withMargin[axis.size],
- );
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should add the first draggable to the drag impact', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [draggable1.id],
- amount: patch(axis.line, draggable2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- // is now in the first position
- index: 0,
- },
- direction: axis.direction,
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
-
- describe('dragging the third item back to the second position', () => {
- const impact: DragImpact = {
- movement: {
- draggables: [],
- amount: patch(axis.line, draggable3.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- index: 2,
- },
- direction: axis.direction,
- };
- const result: ?JumpToNextResult = jumpToNextIndex({
- isMovingForward: false,
- draggableId: draggable3.id,
- impact,
- draggables,
- droppables,
- });
-
- if (!result) {
- throw new Error('invalid result');
- }
-
- it('should return the negated size of draggable2 as the diff', () => {
- const expected: Position = patch(
- axis.line,
- -draggable2.page.withMargin[axis.size],
- );
-
- expect(result.diff).toEqual(expected);
- });
-
- it('should add the second draggable to the drag impact', () => {
- const expected: DragImpact = {
- movement: {
- draggables: [draggable2.id],
- amount: patch(axis.line, draggable3.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- destination: {
- droppableId: droppable.id,
- // is now in the second position
- index: 1,
- },
- direction: axis.direction,
- };
-
- expect(result.impact).toEqual(expected);
- });
- });
- });
- });
- });
- });
-});
diff --git a/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js
new file mode 100644
index 0000000000..ee212c83f8
--- /dev/null
+++ b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js
@@ -0,0 +1,627 @@
+// @flow
+import getBestCrossAxisDroppable from '../../../../src/state/move-cross-axis/get-best-cross-axis-droppable';
+import { getDroppableDimension } from '../../../../src/state/dimension';
+import getClientRect from '../../../../src/state/get-client-rect';
+import { add } from '../../../../src/state/position';
+import { horizontal, vertical } from '../../../../src/state/axis';
+import type {
+ Axis,
+ Position,
+ DroppableDimension,
+ DroppableDimensionMap,
+} from '../../../../src/types';
+
+describe('get best cross axis droppable', () => {
+ describe('on the vertical axis', () => {
+ const axis: Axis = vertical;
+
+ it('should return the first droppable on the cross axis when moving forward', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const forward = getDroppableDimension({
+ id: 'forward',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 30,
+ right: 40,
+ bottom: 10,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [forward.id]: forward,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(forward);
+ });
+
+ it('should return the first droppable on the cross axis when moving backward', () => {
+ const behind = getDroppableDimension({
+ id: 'behind',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 30,
+ right: 40,
+ bottom: 10,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [behind.id]: behind,
+ [source.id]: source,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ // moving backwards
+ isMovingForward: false,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(behind);
+ });
+
+ it('should exclude options that are not in the desired direction', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const behind = getDroppableDimension({
+ id: 'behind',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 10,
+ bottom: 10,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [behind.id]: behind,
+ [source.id]: source,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+ expect(result).toBe(null);
+
+ // checking that it would have been returned if was moving in the other direction
+ const result2: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: false,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+ expect(result2).toBe(behind);
+ });
+
+ it('should exclude options that are not enabled', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const disabled = getDroppableDimension({
+ id: 'disabled',
+ isEnabled: false,
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 30,
+ right: 40,
+ bottom: 10,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [disabled.id]: disabled,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ it('should exclude options that do not overlap on the main axis', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const noOverlap = getDroppableDimension({
+ id: 'noOverlap',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // top is below where the source ended
+ top: 11,
+ left: 30,
+ right: 40,
+ bottom: 20,
+ }),
+ });
+
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [noOverlap.id]: noOverlap,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('more than one option share the same crossAxisStart value', () => {
+ // this happens when two lists sit on top of one another
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 20,
+ bottom: 100,
+ }),
+ });
+ const sibling1 = getDroppableDimension({
+ id: 'sibling1',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // not the same top value as source
+ top: 20,
+ // shares the left edge with the source
+ left: 20,
+ right: 40,
+ bottom: 40,
+ }),
+ });
+ const sibling2 = getDroppableDimension({
+ id: 'sibling2',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // shares the bottom edge with sibling1
+ top: 40,
+ // shares the left edge with the source
+ left: 20,
+ right: 40,
+ bottom: 60,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [sibling1.id]: sibling1,
+ [sibling2.id]: sibling2,
+ };
+
+ it('should return a droppable where the center position (axis.line) of the draggable draggable sits within the size of a droppable', () => {
+ // sitting inside source - but within the size of sibling2 on the main axis
+ const center: Position = {
+ y: 50,
+ x: 10,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling2);
+ });
+
+ describe('center point is not contained within a droppable', () => {
+ it('should return the droppable that has the closest corner', () => {
+ // Choosing a point that is above the first sibling
+ const center: Position = {
+ // above sibling 1
+ y: 10,
+ x: 10,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling1);
+ });
+
+ it('should choose the droppable that is furthest back (closest to {x: 0, y: 0} on the screen) in the event of a tie', () => {
+ // Choosing a point that is above the first sibling
+ const center: Position = {
+ // this line is shared between sibling1 and sibling2
+ y: 40,
+ x: 10,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling1);
+
+ // checking that center position was selected correctly
+ const center2: Position = add(center, { x: 0, y: 1 });
+ const result2: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center2,
+ source,
+ droppables,
+ });
+ expect(result2).toBe(sibling2);
+ });
+ });
+ });
+ });
+
+ describe('on the horizontal axis', () => {
+ const axis: Axis = horizontal;
+
+ it('should return the first droppable on the cross axis when moving forward', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 20,
+ bottom: 20,
+ }),
+ });
+ const forward = getDroppableDimension({
+ id: 'forward',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 20,
+ left: 0,
+ right: 20,
+ bottom: 30,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [forward.id]: forward,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(forward);
+ });
+
+ it('should return the first droppable on the cross axis when moving backward', () => {
+ const behind = getDroppableDimension({
+ id: 'behind',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 20,
+ bottom: 10,
+ }),
+ });
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 10,
+ left: 0,
+ right: 20,
+ bottom: 20,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [behind.id]: behind,
+ [source.id]: source,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ // moving backwards
+ isMovingForward: false,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(behind);
+ });
+
+ it('should exclude options that are not in the desired direction', () => {
+ const behind = getDroppableDimension({
+ id: 'behind',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 20,
+ bottom: 10,
+ }),
+ });
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 10,
+ left: 0,
+ right: 20,
+ bottom: 20,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [behind.id]: behind,
+ [source.id]: source,
+ };
+
+ // now moving in the other direction
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+ expect(result).toBe(null);
+
+ // Ensuring that normally it would be returned if moving in the right direction
+ const result2: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: false,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+ expect(result2).toBe(behind);
+ });
+
+ it('should exclude options that are not enabled', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 20,
+ right: 30,
+ bottom: 10,
+ }),
+ });
+ const disabled = getDroppableDimension({
+ id: 'disabled',
+ isEnabled: false,
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 30,
+ right: 40,
+ bottom: 10,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [disabled.id]: disabled,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ it('should exclude options that do not overlap on the main axis', () => {
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 20,
+ bottom: 10,
+ }),
+ });
+ const noOverlap = getDroppableDimension({
+ id: 'noOverlap',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // comes after the source
+ top: 10,
+ // but its left value is > the rigt of the source
+ left: 30,
+ right: 40,
+ bottom: 20,
+ }),
+ });
+
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [noOverlap.id]: noOverlap,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: source.page.withMargin.center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('more than one option share the same crossAxisStart value', () => {
+ // this happens when two lists sit side by side
+ const source = getDroppableDimension({
+ id: 'source',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 10,
+ }),
+ });
+ const sibling1 = getDroppableDimension({
+ id: 'sibling1',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // shares an edge with the source
+ top: 10,
+ // shares the left edge with the source
+ left: 20,
+ right: 40,
+ bottom: 20,
+ }),
+ });
+ const sibling2 = getDroppableDimension({
+ id: 'sibling2',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ // shares an edge with the source
+ top: 10,
+ // shares the left edge with the source
+ left: 40,
+ right: 60,
+ bottom: 20,
+ }),
+ });
+ const droppables: DroppableDimensionMap = {
+ [source.id]: source,
+ [sibling1.id]: sibling1,
+ [sibling2.id]: sibling2,
+ };
+
+ it('should return a droppable where the center position (axis.line) of the draggable draggable sits within the size of a droppable', () => {
+ // sitting inside source - but within the size of sibling2 on the main axis
+ const center: Position = {
+ y: 5,
+ x: 50,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling2);
+ });
+
+ describe('center point is not contained within a droppable', () => {
+ it('should return the droppable that has the closest corner', () => {
+ // Choosing a point that is before the first sibling
+ const center: Position = {
+ // above sibling 1
+ y: 5,
+ // before the left value of sibling 1
+ x: 10,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling1);
+ });
+
+ it('should choose the droppable that is furthest back (closest to {x: 0, y: 0} on the screen) in the event of a tie', () => {
+ // Choosing a point that is above the first sibling
+ const center: Position = {
+ y: 5,
+ // this line is shared between sibling1 and sibling2
+ x: 40,
+ };
+
+ const result: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center,
+ source,
+ droppables,
+ });
+
+ expect(result).toBe(sibling1);
+
+ // checking that center point is correct
+
+ const center2: Position = add(center, { y: 0, x: 1 });
+ const result2: ?DroppableDimension = getBestCrossAxisDroppable({
+ isMovingForward: true,
+ pageCenter: center2,
+ source,
+ droppables,
+ });
+
+ expect(result2).toBe(sibling2);
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js
new file mode 100644
index 0000000000..676b2573dd
--- /dev/null
+++ b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js
@@ -0,0 +1,243 @@
+// @flow
+import getClosestDraggable from '../../../../src/state/move-cross-axis/get-closest-draggable';
+import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension';
+import { add, distance, patch } from '../../../../src/state/position';
+import { horizontal, vertical } from '../../../../src/state/axis';
+import getClientRect from '../../../../src/state/get-client-rect';
+import type {
+ Axis,
+ Position,
+ DraggableDimension,
+ DroppableDimension,
+} from '../../../../src/types';
+
+describe('get closest draggable', () => {
+ [vertical, horizontal].forEach((axis: Axis) => {
+ const start: number = 0;
+ const end: number = 100;
+ const crossAxisStart: number = 0;
+ const crossAxisEnd: number = 20;
+
+ const droppable: DroppableDimension = getDroppableDimension({
+ id: 'droppable',
+ clientRect: getClientRect({
+ [axis.start]: start,
+ [axis.end]: end,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ }),
+ });
+
+ // first item bleeds backwards past the start of the droppable
+ const partialHiddenBackwards: DraggableDimension = getDraggableDimension({
+ id: 'partialHiddenBackwards',
+ droppableId: droppable.id,
+ clientRect: getClientRect({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: -10, // -10
+ [axis.end]: 20,
+ }),
+ });
+
+ const visible1: DraggableDimension = getDraggableDimension({
+ id: 'visible1',
+ droppableId: droppable.id,
+ clientRect: getClientRect({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 20,
+ [axis.end]: 40,
+ }),
+ });
+
+ const visible2: DraggableDimension = getDraggableDimension({
+ id: 'visible2',
+ droppableId: droppable.id,
+ clientRect: getClientRect({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 40,
+ [axis.end]: 60,
+ }),
+ });
+
+ // bleeds over the end of the visible boundary
+ const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({
+ id: 'partiallyHiddenForwards',
+ droppableId: droppable.id,
+ clientRect: getClientRect({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 60,
+ [axis.end]: 120,
+ }),
+ });
+
+ // totally invisible
+ const hidden: DraggableDimension = getDraggableDimension({
+ id: 'hidden',
+ droppableId: droppable.id,
+ clientRect: getClientRect({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 120,
+ [axis.end]: 140,
+ }),
+ });
+
+ const insideDestination: DraggableDimension[] = [
+ partialHiddenBackwards,
+ visible1,
+ visible2,
+ partiallyHiddenForwards,
+ hidden,
+ ];
+
+ it('should return the closest draggable', () => {
+ // closet to visible1
+ const center1: Position = patch(
+ axis.line, visible1.page.withoutMargin.center[axis.line], 100
+ );
+ const result1: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center1,
+ destination: droppable,
+ insideDestination,
+ });
+ expect(result1).toBe(visible1);
+
+ // closest to visible2
+ const center2: Position = patch(
+ axis.line, visible2.page.withoutMargin.center[axis.line], 100
+ );
+ const result2: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center2,
+ destination: droppable,
+ insideDestination,
+ });
+ expect(result2).toBe(visible2);
+ });
+
+ it('should return null if there are no draggables in the droppable', () => {
+ const center: Position = {
+ x: 100,
+ y: 100,
+ };
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination: [],
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => {
+ it('should ignore draggables that have backwards partial visiblility', () => {
+ // point would usually be closest to visible1 -
+ // but it is outside of the visible bounds of the droppable
+ const center: Position = patch(
+ axis.line, partialHiddenBackwards.page.withoutMargin.center[axis.line], 100
+ );
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination,
+ });
+
+ expect(result).toBe(visible1);
+ });
+
+ it('should ignore draggables that have forward partial visiblility', () => {
+ const center: Position = patch(
+ axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100
+ );
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination,
+ });
+
+ expect(result).toBe(visible2);
+ });
+
+ it('should ignore draggables that have no visiblity', () => {
+ const center: Position = patch(
+ axis.line, hidden.page.withoutMargin.center[axis.line], 100
+ );
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination,
+ });
+
+ expect(result).toBe(visible2);
+ });
+
+ it('should return null if there are no visible targets', () => {
+ const notVisible: DraggableDimension[] = [
+ partialHiddenBackwards,
+ partiallyHiddenForwards,
+ hidden,
+ ];
+ const center: Position = {
+ x: 0,
+ y: 0,
+ };
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination: notVisible,
+ });
+
+ expect(result).toBe(null);
+ });
+ });
+
+ it('should return the draggable that is first on the main axis in the event of a tie', () => {
+ // in this case the distance between visible1 and visible2 is the same
+ const center: Position = patch(
+ axis.line,
+ // this is shared edge
+ visible2.page.withoutMargin[axis.start],
+ 100
+ );
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination,
+ });
+
+ expect(result).toBe(visible1);
+
+ // validating test assumptions
+
+ // 1. that they have equal distances
+ expect(distance(center, visible1.page.withoutMargin.center))
+ .toEqual(distance(center, visible2.page.withoutMargin.center));
+
+ // 2. if we move beyond the edge visible2 will be selected
+ const result2: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: add(center, patch(axis.line, 1)),
+ destination: droppable,
+ insideDestination,
+ });
+ expect(result2).toBe(visible2);
+ });
+ });
+});
diff --git a/test/unit/state/move-cross-axis/move-cross-axis.spec.js b/test/unit/state/move-cross-axis/move-cross-axis.spec.js
new file mode 100644
index 0000000000..05ff26ee3c
--- /dev/null
+++ b/test/unit/state/move-cross-axis/move-cross-axis.spec.js
@@ -0,0 +1,98 @@
+// @flow
+import moveCrossAxis from '../../../../src/state/move-cross-axis/';
+import type { Result } from '../../../../src/state/move-cross-axis/move-cross-axis-types';
+import getDroppableWithDraggables from '../../../utils/get-droppable-with-draggables';
+import type { Result as Data } from '../../../utils/get-droppable-with-draggables';
+import type {
+ DraggableDimension,
+ DraggableDimensionMap,
+ DroppableDimensionMap,
+} from '../../../../src/types';
+
+// The functionality of move-cross-axis is covered by other files in this folder.
+// This spec file is directed any any logic in move-cross-axis/index.js
+
+describe('move cross axis', () => {
+ const home: Data = getDroppableWithDraggables({
+ droppableId: 'home',
+ droppableRect: { top: 0, left: 0, right: 100, bottom: 100 },
+ draggableRects: [
+ { top: 0, left: 0, right: 100, bottom: 20 },
+ ],
+ });
+
+ it('should return null if there are draggables in a destination list but none are visible', () => {
+ const foreign: Data = getDroppableWithDraggables({
+ droppableId: 'foreign',
+ // to the right of home
+ droppableRect: { top: 0, left: 100, right: 200, bottom: 100 },
+ draggableRects: [
+ // bigger than that visible rect
+ { top: 0, left: 100, right: 200, bottom: 110 },
+ ],
+ });
+ const draggables: DraggableDimensionMap = {
+ ...home.draggables,
+ ...foreign.draggables,
+ };
+ const droppables: DroppableDimensionMap = {
+ [home.droppable.id]: home.droppable,
+ [foreign.droppable.id]: foreign.droppable,
+ };
+ const draggable: DraggableDimension = home.draggableDimensions[0];
+
+ const result: ?Result = moveCrossAxis({
+ isMovingForward: true,
+ pageCenter: draggable.page.withMargin.center,
+ draggableId: draggable.id,
+ droppableId: home.droppable.id,
+ home: {
+ droppableId: home.droppable.id,
+ index: 0,
+ },
+ draggables,
+ droppables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ // this test is a validation that the previous test is working correctly
+ it('should return a droppable if its children are visible (and all other criteria are met', () => {
+ // adding visible child to foreign
+ const foreign: Data = getDroppableWithDraggables({
+ droppableId: 'foreign',
+ // to the right of home
+ droppableRect: { top: 0, left: 100, right: 200, bottom: 100 },
+ draggableRects: [
+ // child is visible
+ { top: 0, left: 100, right: 200, bottom: 90 },
+ ],
+ });
+ const draggables: DraggableDimensionMap = {
+ ...home.draggables,
+ ...foreign.draggables,
+ };
+ const droppables: DroppableDimensionMap = {
+ [home.droppable.id]: home.droppable,
+ [foreign.droppable.id]: foreign.droppable,
+ };
+ const draggable: DraggableDimension = home.draggableDimensions[0];
+
+ const result: ?Result = moveCrossAxis({
+ isMovingForward: true,
+ pageCenter: draggable.page.withMargin.center,
+ draggableId: draggable.id,
+ droppableId: home.droppable.id,
+ home: {
+ droppableId: home.droppable.id,
+ index: 0,
+ },
+ draggables,
+ droppables,
+ });
+
+ // not asserting anything about the behaviour - just that something was returned
+ expect(result).toBeTruthy();
+ });
+});
diff --git a/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js
new file mode 100644
index 0000000000..de4137fe64
--- /dev/null
+++ b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js
@@ -0,0 +1,534 @@
+// @flow
+import moveToNewDroppable from '../../../../src/state/move-cross-axis/move-to-new-droppable/';
+import type { Result } from '../../../../src/state/move-cross-axis/move-cross-axis-types';
+import { getDraggableDimension, getDroppableDimension } from '../../../../src/state/dimension';
+import getClientRect from '../../../../src/state/get-client-rect';
+import moveToEdge from '../../../../src/state/move-to-edge';
+import { patch } from '../../../../src/state/position';
+import { horizontal, vertical } from '../../../../src/state/axis';
+import type {
+ Axis,
+ Spacing,
+ DragImpact,
+ DraggableDimension,
+ DroppableDimension,
+ Position,
+} from '../../../../src/types';
+
+describe('move to new droppable', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ console.error.mockRestore();
+ });
+
+ const noMargin: Spacing = { top: 0, left: 0, bottom: 0, right: 0 };
+
+ [vertical, horizontal].forEach((axis: Axis) => {
+ describe(`on ${axis.direction} axis`, () => {
+ const margin = {
+ ...noMargin,
+ [axis.end]: 10,
+ };
+
+ const crossAxisStart: number = 0;
+ const crossAxisEnd: number = 100;
+
+ const home: DroppableDimension = getDroppableDimension({
+ id: 'home',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.end]: 200,
+ }),
+ });
+ // size: 10
+ const inHome1: DraggableDimension = getDraggableDimension({
+ id: 'inhome1',
+ droppableId: home.id,
+ margin,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.end]: 10,
+ }),
+ });
+ // size: 20
+ const inHome2: DraggableDimension = getDraggableDimension({
+ id: 'inhome2',
+ droppableId: home.id,
+ // pushed forward by margin of inHome1
+ margin,
+ clientRect: getClientRect({
+ [axis.start]: 20,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.end]: 50,
+ }),
+ });
+ // size: 30
+ const inHome3: DraggableDimension = getDraggableDimension({
+ id: 'inhome3',
+ droppableId: home.id,
+ margin,
+ // pushed forward by margin of inHome2
+ clientRect: getClientRect({
+ [axis.start]: 60,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.end]: 90,
+ }),
+ });
+ // size: 40
+ const inHome4: DraggableDimension = getDraggableDimension({
+ id: 'inhome4',
+ droppableId: home.id,
+ // pushed forward by margin of inHome3
+ margin,
+ clientRect: getClientRect({
+ [axis.start]: 100,
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.end]: 140,
+ }),
+ });
+
+ // TODO: get working with horizonital axis
+ describe('to home list', () => {
+ const dontCare: Position = { x: 0, y: 0 };
+ const draggables: DraggableDimension[] = [
+ inHome1, inHome2, inHome3, inHome4,
+ ];
+
+ it('should return null and log an error if no target is found', () => {
+ // this should never happen but just being safe
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome1,
+ target: null,
+ destination: home,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ expect(result).toBe(null);
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should return null and log an error if the target is not inside the droppable', () => {
+ const invalid: DraggableDimension = getDraggableDimension({
+ id: 'invalid',
+ droppableId: 'some-other-droppable',
+ clientRect: getClientRect({
+ top: 1000,
+ left: 1000,
+ bottom: 1100,
+ right: 1100,
+ }),
+ });
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: draggables[0],
+ target: invalid,
+ destination: home,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ expect(result).toBe(null);
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ describe('moving back into original index', () => {
+ // the second draggable is moving back into its home
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome2,
+ target: inHome2,
+ destination: home,
+ insideDestination: draggables,
+ home: {
+ index: 1,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should return the original center without margin', () => {
+ expect(result.pageCenter).toBe(inHome2.page.withoutMargin.center);
+ expect(result.pageCenter).not.toEqual(inHome2.page.withMargin.center);
+ });
+
+ it('should return an empty impact with the original location', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, inHome2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('moving before the original index', () => {
+ // moving inHome4 into the inHome2 position
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome4,
+ target: inHome2,
+ destination: home,
+ insideDestination: draggables,
+ home: {
+ index: 3,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should align to the start of the target', () => {
+ const expected: Position = moveToEdge({
+ source: inHome4.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inHome2.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move the everything from the target index to the original index forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ draggables: [inHome2.id, inHome3.id],
+ amount: patch(axis.line, inHome4.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.id,
+ // original index of target
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('moving after the original index', () => {
+ // moving inHome1 into the inHome4 position
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome1,
+ target: inHome4,
+ destination: home,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ describe('center', () => {
+ it('should align to the bottom of the target', () => {
+ const expected: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: inHome4.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+ });
+
+ it('should move the everything from the target index to the original index forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ draggables: [inHome4.id, inHome3.id, inHome2.id],
+ amount: patch(axis.line, inHome1.page.withMargin[axis.size]),
+ // is moving beyond start position
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.id,
+ // original index of target
+ index: 3,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+
+ describe('to foreign list', () => {
+ const foreignCrossAxisStart: number = 100;
+ const foreignCrossAxisEnd: number = 200;
+
+ const foreign: DroppableDimension = getDroppableDimension({
+ id: 'foreign',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.end]: 200,
+ }),
+ });
+ // size: 10
+ const inForeign1: DraggableDimension = getDraggableDimension({
+ id: 'inForeign1',
+ droppableId: foreign.id,
+ margin,
+ clientRect: getClientRect({
+ [axis.start]: 0,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.end]: 10,
+ }),
+ });
+ // size: 20
+ const inForeign2: DraggableDimension = getDraggableDimension({
+ id: 'inForeign2',
+ droppableId: foreign.id,
+ // pushed forward by margin of inForeign1
+ margin,
+ clientRect: getClientRect({
+ [axis.start]: 20,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.end]: 50,
+ }),
+ });
+ // size: 30
+ const inForeign3: DraggableDimension = getDraggableDimension({
+ id: 'inForeign3',
+ droppableId: foreign.id,
+ margin,
+ // pushed forward by margin of inForeign2
+ clientRect: getClientRect({
+ [axis.start]: 60,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.end]: 90,
+ }),
+ });
+ // size: 40
+ const inForeign4: DraggableDimension = getDraggableDimension({
+ id: 'inForeign4',
+ droppableId: foreign.id,
+ margin,
+ // pushed forward by margin of inForeign3
+ clientRect: getClientRect({
+ [axis.start]: 100,
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.end]: 140,
+ }),
+ });
+
+ const draggables: DraggableDimension[] = [
+ inForeign1, inForeign2, inForeign3, inForeign4,
+ ];
+
+ it('should return null when the target is not within the list - cannot really happen', () => {
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: inHome2,
+ destination: foreign,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('moving into an unpopulated list', () => {
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: null,
+ destination: foreign,
+ insideDestination: [],
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move to the start edge of the droppable', () => {
+ const expected: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should return an empty impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ index: 0,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('is moving before the target', () => {
+ // moving home1 into the second position of the list
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: inForeign2,
+ destination: foreign,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move before the target', () => {
+ const expected: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inForeign2.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move the target and everything below it forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ draggables: [inForeign2.id, inForeign3.id, inForeign4.id],
+ amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // index of foreign2
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('is moving after the target', () => {
+ // moving home4 into the second position of the foreign list
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome4.page.withMargin.center,
+ draggable: inHome4,
+ target: inForeign2,
+ destination: foreign,
+ insideDestination: draggables,
+ home: {
+ index: 3,
+ droppableId: home.id,
+ },
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move after the target', () => {
+ const expected = moveToEdge({
+ source: inHome4.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inForeign2.page.withMargin,
+ // going after
+ destinationEdge: 'end',
+ destinationAxis: foreign.axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move everything after the proposed index forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ draggables: [inForeign3.id, inForeign4.id],
+ amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // going after target, so index is target index + 1
+ index: 2,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js
new file mode 100644
index 0000000000..10ed9fc01a
--- /dev/null
+++ b/test/unit/state/move-to-edge.spec.js
@@ -0,0 +1,272 @@
+// @flow
+import {
+ add,
+ absolute,
+ isEqual,
+ patch,
+ subtract,
+} from '../../../src/state/position';
+import getFragment from '../../utils/get-fragment';
+import getClientRect from '../../../src/state/get-client-rect';
+import moveToEdge from '../../../src/state/move-to-edge';
+import { vertical, horizontal } from '../../../src/state/axis';
+import type {
+ Axis,
+ Position,
+ DimensionFragment,
+} from '../../../src/types';
+
+// behind the destination
+// width: 40, height: 20
+const behind: DimensionFragment = getFragment(getClientRect({
+ top: 0,
+ left: 0,
+ right: 40,
+ bottom: 20,
+}));
+
+// in front of the destination
+// width: 50, height: 10
+const infront: DimensionFragment = getFragment(getClientRect({
+ top: 120,
+ left: 150,
+ right: 200,
+ bottom: 130,
+}));
+
+// width: 50, height: 60
+const destination: DimensionFragment = getFragment(getClientRect({
+ top: 50,
+ left: 50,
+ right: 100,
+ bottom: 110,
+}));
+
+// All results are aligned on the crossAxisStart
+
+const pullBackwardsOnMainAxis = (axis: Axis) => (point: Position) => patch(
+ axis.line,
+ -point[axis.line],
+ point[axis.crossLine]
+);
+
+// returns the absolute difference of the center position
+// to one of the corners on the axis.end. Choosing axis.end is arbitrary
+const getCenterDiff = (axis: Axis) => (source: DimensionFragment): Position => {
+ const corner = patch(
+ axis.line, source[axis.end], source[axis.crossAxisStart]
+ );
+
+ const diff = absolute(subtract(source.center, corner));
+
+ (() => {
+ // a little check to ensure that our assumption that the distance between the edges
+ // and the axis.end is the same
+ const otherCorner = patch(
+ axis.line, source[axis.end], source[axis.crossAxisEnd]
+ );
+ const otherDiff = absolute(subtract(source.center, otherCorner));
+
+ if (!isEqual(diff, otherDiff)) {
+ throw new Error('invalidation position assumption');
+ }
+ })();
+
+ return diff;
+};
+
+describe('move to edge', () => {
+ [behind, infront].forEach((source: DimensionFragment) => {
+ describe(`source is ${source === behind ? 'behind' : 'infront of'} destination`, () => {
+ describe('moving to a vertical list', () => {
+ const pullUpwards = pullBackwardsOnMainAxis(vertical);
+ const centerDiff = getCenterDiff(vertical)(source);
+
+ describe('destination start edge', () => {
+ const destinationTopCorner: Position = {
+ x: destination.left,
+ y: destination.top,
+ };
+
+ describe('to source end edge', () => {
+ it('should move the source above the destination', () => {
+ const newCenter: Position = add(
+ pullUpwards(centerDiff),
+ destinationTopCorner
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'end',
+ destination,
+ destinationEdge: 'start',
+ destinationAxis: vertical,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+
+ describe('to source start edge', () => {
+ it('should move below the top of the destination', () => {
+ const newCenter: Position = add(
+ centerDiff,
+ destinationTopCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'start',
+ destination,
+ destinationEdge: 'start',
+ destinationAxis: vertical,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+ });
+
+ describe('destination end edge', () => {
+ const destinationBottomCorner: Position = {
+ x: destination.left,
+ y: destination.bottom,
+ };
+
+ describe('to source end edge', () => {
+ it('should move above the bottom of the destination', () => {
+ const newCenter: Position = add(
+ pullUpwards(centerDiff),
+ destinationBottomCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'end',
+ destination,
+ destinationEdge: 'end',
+ destinationAxis: vertical,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+
+ describe('to source start edge', () => {
+ it('should move below the destination', () => {
+ const newCenter: Position = add(
+ centerDiff,
+ destinationBottomCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'start',
+ destination,
+ destinationEdge: 'end',
+ destinationAxis: vertical,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+ });
+ });
+
+ describe('moving to a horizontal list', () => {
+ const pullLeft = pullBackwardsOnMainAxis(horizontal);
+ const centerDiff = getCenterDiff(horizontal)(source);
+
+ describe('destination start edge', () => {
+ const destinationTopCorner: Position = {
+ x: destination.left, // axis.start
+ y: destination.top, // axis.crossAxisStart
+ };
+
+ describe('to source end edge', () => {
+ it('should move the source to the left of destination start edge', () => {
+ const newCenter: Position = add(
+ pullLeft(centerDiff),
+ destinationTopCorner
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'end',
+ destination,
+ destinationEdge: 'start',
+ destinationAxis: horizontal,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+
+ describe('to source start edge', () => {
+ it('should move to the right of the destination start edge', () => {
+ const newCenter: Position = add(
+ centerDiff,
+ destinationTopCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'start',
+ destination,
+ destinationEdge: 'start',
+ destinationAxis: horizontal,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+ });
+
+ describe('destination end edge', () => {
+ const destinationTopRightCorner: Position = {
+ x: destination.right, // axis.end
+ y: destination.top, // axis.crossAxisStart
+ };
+
+ describe('to source end edge', () => {
+ it('should move to the left of right side of the destination', () => {
+ const newCenter: Position = add(
+ pullLeft(centerDiff),
+ destinationTopRightCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'end',
+ destination,
+ destinationEdge: 'end',
+ destinationAxis: horizontal,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+
+ describe('to source start edge', () => {
+ it('should move to the right of the destination', () => {
+ const newCenter: Position = add(
+ centerDiff,
+ destinationTopRightCorner,
+ );
+
+ const result: Position = moveToEdge({
+ source,
+ sourceEdge: 'start',
+ destination,
+ destinationEdge: 'end',
+ destinationAxis: horizontal,
+ });
+
+ expect(result).toEqual(newCenter);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js
new file mode 100644
index 0000000000..a268a7aa01
--- /dev/null
+++ b/test/unit/state/move-to-next-index.spec.js
@@ -0,0 +1,1106 @@
+// @flow
+import moveToNextIndex from '../../../src/state/move-to-next-index/';
+import type { Result } from '../../../src/state/move-to-next-index/move-to-next-index-types';
+import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension';
+import getClientRect from '../../../src/state/get-client-rect';
+import moveToEdge from '../../../src/state/move-to-edge';
+import { patch } from '../../../src/state/position';
+import { vertical, horizontal } from '../../../src/state/axis';
+import type {
+ Axis,
+ DragMovement,
+ DragImpact,
+ DraggableDimension,
+ DroppableDimension,
+ DraggableDimensionMap,
+ DraggableLocation,
+ Position,
+} from '../../../src/types';
+
+describe('move to next index', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ console.error.mockRestore();
+ });
+
+ [vertical, horizontal].forEach((axis: Axis) => {
+ describe(`on the ${axis.direction} axis`, () => {
+ const home: DroppableDimension = getDroppableDimension({
+ id: 'home',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ bottom: 1000,
+ right: 1000,
+ }),
+ });
+
+ // size: 100
+ const home1: DraggableDimension = getDraggableDimension({
+ id: 'home1',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ top: 0,
+ left: 0,
+ bottom: 100,
+ right: 100,
+ }),
+ });
+
+ // size: 199
+ const home2: DraggableDimension = getDraggableDimension({
+ id: 'home2',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ top: 101,
+ left: 101,
+ bottom: 300,
+ right: 300,
+ }),
+ });
+
+ // size: 299
+ const home3: DraggableDimension = getDraggableDimension({
+ id: 'home3',
+ droppableId: home.id,
+ clientRect: getClientRect({
+ top: 301,
+ left: 301,
+ bottom: 600,
+ right: 600,
+ }),
+ });
+
+ // foreign droppable
+ const foreign: DroppableDimension = getDroppableDimension({
+ id: 'foreign',
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 1001,
+ left: 1001,
+ bottom: 2000,
+ right: 2000,
+ }),
+ });
+
+ // size: 99
+ const foreign1: DraggableDimension = getDraggableDimension({
+ id: 'foreign1',
+ droppableId: foreign.id,
+ clientRect: getClientRect({
+ top: 1001,
+ left: 1001,
+ bottom: 1100,
+ right: 1100,
+ }),
+ });
+
+ // size: 199
+ const foreign2: DraggableDimension = getDraggableDimension({
+ id: 'foreign2',
+ droppableId: foreign.id,
+ clientRect: getClientRect({
+ top: 1101,
+ left: 1101,
+ bottom: 1300,
+ right: 1300,
+ }),
+ });
+
+ // size: 299
+ const foreign3: DraggableDimension = getDraggableDimension({
+ id: 'foreign3',
+ droppableId: foreign.id,
+ clientRect: getClientRect({
+ top: 1301,
+ left: 1301,
+ bottom: 1600,
+ right: 1600,
+ }),
+ });
+
+ const draggables: DraggableDimensionMap = {
+ [home1.id]: home1,
+ [home2.id]: home2,
+ [home3.id]: home3,
+ [foreign1.id]: foreign1,
+ [foreign2.id]: foreign2,
+ [foreign3.id]: foreign3,
+ };
+
+ it('should return null if the droppable is disabled', () => {
+ const disabled: DroppableDimension = getDroppableDimension({
+ id: 'disabled',
+ isEnabled: false,
+ direction: axis.direction,
+ clientRect: getClientRect({
+ top: 2001,
+ left: 2001,
+ bottom: 3000,
+ right: 3000,
+ }),
+ });
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: disabled.id,
+ index: 0,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: disabled,
+ draggables,
+ });
+
+ expect(result).toEqual(null);
+ });
+
+ it('should return null if there was no previous destination', () => {
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ // no previous destination - should not happen when dragging with a keyboard
+ destination: null,
+ };
+
+ const result1: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ expect(result1).toEqual(null);
+ expect(console.error).toHaveBeenCalledTimes(1);
+
+ const result2: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ expect(result2).toEqual(null);
+ expect(console.error).toHaveBeenCalledTimes(2);
+ });
+
+ describe('in home list', () => {
+ describe('moving forwards', () => {
+ it('should return null if cannot move forward', () => {
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ index: 2,
+ droppableId: home.id,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home3.id,
+ impact,
+ droppable: home,
+ draggables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('is moving away from start position', () => {
+ describe('dragging first item forward one position', () => {
+ // dragging the first item forward into the second position
+ const movement: DragMovement = {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ };
+ const destination: DraggableLocation = {
+ index: 0,
+ droppableId: home.id,
+ };
+ const impact: DragImpact = {
+ movement,
+ direction: axis.direction,
+ destination,
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the end of the dragging item to the end of the next item', () => {
+ const expected: Position = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move the item into the second spot and move the second item out of the way', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [home2.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // is now in the second position
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('dragging second item forward one position', () => {
+ const movement: DragMovement = {
+ draggables: [],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ };
+ const destination: DraggableLocation = {
+ index: 1,
+ droppableId: home.id,
+ };
+ const impact: DragImpact = {
+ movement,
+ direction: axis.direction,
+ destination,
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home2.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the end of the dragging item to the end of the next item', () => {
+ const expected: Position = moveToEdge({
+ source: home2.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: home3.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move the dragging item into the third spot and move the third item out of the way', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [home3.id],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // is now in the second position
+ destination: {
+ droppableId: home.id,
+ index: 2,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('dragging first item forward one position after already moving it forward once', () => {
+ const impact: DragImpact = {
+ movement: {
+ // second item has already moved
+ draggables: [home2.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // draggable1 is now in the second position
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the end of the dragging item to the end of the next item', () => {
+ // next dimension from the current index is draggable3
+ const expected: Position = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: home3.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should move the third item out of the way', () => {
+ const expected: DragImpact = {
+ movement: {
+ // adding draggable3 to the list
+ // list is sorted by the the closest to the current item
+ draggables: [home3.id, home2.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // is now in the second position
+ destination: {
+ droppableId: home.id,
+ index: 2,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+
+ describe('is moving toward start position', () => {
+ describe('dragging item forward to starting position', () => {
+ // dragging the second item (draggable2), which has previously
+ // been moved backwards and is now in the first position
+ const impact: DragImpact = {
+ movement: {
+ draggables: [home1.id],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ index: 0,
+ droppableId: home.id,
+ },
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home2.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result of moveToNextIndex');
+ }
+
+ it('should move the start of the dragging item to the end of the previous item (which its original position)', () => {
+ const expected: Position = moveToEdge({
+ source: home2.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ // is now back at its original position
+ expect(result.pageCenter).toEqual(home2.page.withoutMargin.center);
+ });
+
+ it('should return an empty impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('dragging forwards, but not beyond the starting position', () => {
+ // draggable3 has moved backwards past draggable2 and draggable1
+ const impact: DragImpact = {
+ movement: {
+ // second and first item have already moved
+ // sorted by the draggable that is closest to where the dragging item is
+ draggables: [home1.id, home2.id],
+ amount: patch(axis.line, home3.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // draggable3 is now in the first position
+ destination: {
+ droppableId: home.id,
+ index: 0,
+ },
+ };
+ // moving draggable3 forward one position
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home3.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move to the start of the draggable item to the start position of the destination draggable', () => {
+ const expected: Position = moveToEdge({
+ source: home3.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should remove the first dimension from the impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [home2.id],
+ amount: patch(axis.line, home3.page.withMargin[axis.size]),
+ // is still behind where it started
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ // is now in the second position
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+ });
+
+ describe('moving backwards', () => {
+ it('should return null if cannot move backward', () => {
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ index: 0,
+ droppableId: home.id,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home1.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('is moving away from start position', () => {
+ describe('dragging the second item back to the first position', () => {
+ // no impact yet
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home2.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the start of the draggable to the start of the previous draggable', () => {
+ const expected: Position = moveToEdge({
+ source: home2.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: home1.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should add the first draggable to the drag impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [home1.id],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ // is now in the first position
+ index: 0,
+ },
+ direction: axis.direction,
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('dragging the third item back to the second position', () => {
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home3.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ index: 2,
+ },
+ direction: axis.direction,
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home3.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the start of the draggable to the start of the previous draggable', () => {
+ const expected: Position = moveToEdge({
+ source: home3.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should add the second draggable to the drag impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [home2.id],
+ amount: patch(axis.line, home3.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ // is now in the second position
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+
+ describe('is moving towards the start position', () => {
+ describe('moving back to original position', () => {
+ // dragged the second item (draggable2) forward once, and is now
+ // moving backwards towards the start again
+ const impact: DragImpact = {
+ movement: {
+ draggables: [home3.id],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ destination: {
+ index: 2,
+ droppableId: home.id,
+ },
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home2.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the end of the draggable to the end of the next draggable (which is its original position)', () => {
+ const expected: Position = moveToEdge({
+ source: home2.page.withoutMargin,
+ sourceEdge: 'end',
+ // destination is itself as moving back to home
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ // moved back to its original position
+ expect(result.pageCenter).toEqual(home2.page.withoutMargin.center);
+ });
+
+ it('should return an empty impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('moving back, but not far enough to be at the start yet', () => {
+ // dragged the first item:
+ // forward twice so it is in the third position
+ // then moving backward so it is in the second position
+ const impact: DragImpact = {
+ movement: {
+ // sorted by closest to where the draggable currently is
+ draggables: [home3.id, home2.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ destination: {
+ index: 2,
+ droppableId: home.id,
+ },
+ };
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home1.id,
+ impact,
+ draggables,
+ droppable: home,
+ });
+
+ if (!result) {
+ throw new Error('invalid result');
+ }
+
+ it('should move the end of the draggable to the end of the previous draggable', () => {
+ const expected: Position = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: home2.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should remove the third draggable from the drag impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ // draggable3 has been removed
+ draggables: [home2.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ destination: {
+ droppableId: home.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+ });
+ });
+
+ describe('in foreign list', () => {
+ describe('moving forwards', () => {
+ describe('moving forward one position', () => {
+ // moved home1 into the first position of the foreign list
+ const impact: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ // Because we have moved into the first position it will be ordered 1-2-3
+ draggables: [foreign1.id, foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ // Always false when in another list
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ // it is now in the foreign droppable in the first position
+ droppableId: foreign.id,
+ index: 0,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move to the start edge of the dragging item to the start of foreign2', () => {
+ const expected = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign2.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should remove foreign1 when moving forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('moving into last position of the list', () => {
+ // moved home1 into the second last position of the list
+ const impact: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ draggables: [foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ // Always false when in another list
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ // it is now in the foreign droppable in the third position
+ droppableId: foreign.id,
+ index: 2,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move to the start edge of the dragging item to the end of foreign1', () => {
+ const expected = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign3.page.withMargin,
+ destinationEdge: 'end',
+ destinationAxis: foreign.axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should remove foreign3 when moving forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // bigger than the original list - in the forth position
+ index: 3,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ it('should return null if attempting to move beyond end of the list', () => {
+ // home1 is now in the last position of the list
+ const impact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // bigger than the original list - in the forth position
+ index: 3,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: true,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ expect(result).toBe(null);
+ });
+ });
+
+ describe('moving backwards', () => {
+ it('should return null if attempting to move backwards beyond the start of the list', () => {
+ // moved home1 into the first position of the foreign list
+ const impact: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ // Because we have moved into the first position it will be ordered 1-2-3
+ draggables: [foreign1.id, foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ // Always false when in another list
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ // it is now in the foreign droppable in the first position
+ droppableId: foreign.id,
+ index: 0,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ expect(result).toBe(null);
+ });
+
+ describe('moving backwards one position in list', () => {
+ // home1 is in the third position for foreign (one before the last)
+ const impact: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ draggables: [foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ index: 2,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move to the start edge of foreign2', () => {
+ const expected: Position = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign2.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should add foreign2 to the drag impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ draggables: [foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // moved backwards
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
+
+ describe('moving backwards into the first position of the list', () => {
+ // currently home1 is in the second position in front of foreign1
+ const impact: DragImpact = {
+ movement: {
+ // Ordered by the closest impacted.
+ draggables: [foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ index: 1,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: home1.id,
+ impact,
+ droppable: foreign,
+ draggables,
+ });
+
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move the start edge of home1 to the start edge of foreign1', () => {
+ const expected: Position = moveToEdge({
+ source: home1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign1.page.withoutMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should add foreign1 to the impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ draggables: [foreign1.id, foreign2.id, foreign3.id],
+ amount: patch(axis.line, home1.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: foreign.id,
+ // now in the first position
+ index: 0,
+ },
+ };
+ expect(result.impact).toEqual(expected);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js
index dde74ee3d7..9f9c7ac614 100644
--- a/test/unit/state/position.spec.js
+++ b/test/unit/state/position.spec.js
@@ -1,5 +1,13 @@
// @flow
-import { add, subtract, isEqual, negate, patch } from '../../../src/state/position';
+import {
+ add,
+ subtract,
+ isEqual,
+ negate,
+ patch,
+ distance,
+ closest,
+} from '../../../src/state/position';
import type { Position } from '../../../src/types';
const point1: Position = {
@@ -10,6 +18,7 @@ const point2: Position = {
x: 2,
y: 1,
};
+const origin: Position = { x: 0, y: 0 };
describe('position', () => {
describe('add', () => {
@@ -63,5 +72,57 @@ describe('position', () => {
it('should patch a position with a x value', () => {
expect(patch('y', 5)).toEqual({ x: 0, y: 5 });
});
+
+ it('should allow patching of the non primary line', () => {
+ expect(patch('x', 5, 1)).toEqual({ x: 5, y: 1 });
+ expect(patch('y', 5, 1)).toEqual({ x: 1, y: 5 });
+ });
+ });
+
+ describe('distance', () => {
+ describe('on the same axis', () => {
+ it('should return the distance between two positive values', () => {
+ const a = { x: 0, y: 2 };
+ const b = { x: 0, y: 5 };
+ expect(distance(a, b)).toEqual(3);
+ });
+
+ it('should return the distance between two negative values', () => {
+ const a = { x: 0, y: -2 };
+ const b = { x: 0, y: -5 };
+ expect(distance(a, b)).toEqual(3);
+ });
+
+ it('should return the distance between a positive and negative value', () => {
+ const a = { x: 0, y: -2 };
+ const b = { x: 0, y: 3 };
+ expect(distance(a, b)).toEqual(5);
+ });
+ });
+
+ describe('with axis shift', () => {
+ it('should account for a shift in plane', () => {
+ // a '3, 4, 5' triangle
+ // https://www.mathsisfun.com/pythagoras.html
+ const target = { x: 3, y: 4 };
+ expect(distance(origin, target)).toEqual(5);
+ });
+
+ it('should account for a negative shift in plane', () => {
+ // a reverse '3, 4, 5' triangle shifted down to (-1, -1)
+ const customOrigin = { x: -1, y: -1 };
+ const target = { x: -4, y: -5 };
+ expect(distance(customOrigin, target)).toEqual(5);
+ });
+ });
+ });
+
+ describe('closest', () => {
+ it('should return the closest distance from a series of options', () => {
+ const option1 = { x: 1, y: 1 };
+ const option2 = { x: 2, y: 2 };
+
+ expect(closest(origin, [option1, option2])).toEqual(distance(origin, option1));
+ });
});
});
diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js
index 57808ac280..2981470ce6 100644
--- a/test/unit/view/connected-draggable.spec.js
+++ b/test/unit/view/connected-draggable.spec.js
@@ -6,7 +6,7 @@ import Draggable, { makeSelector } from '../../../src/view/draggable/connected-d
import { getDraggableDimension } from '../../../src/state/dimension';
import noImpact from '../../../src/state/no-impact';
import { combine, withStore, withDroppableId } from '../../utils/get-context-options';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import { add } from '../../../src/state/position';
import type {
CurrentDrag,
@@ -99,6 +99,7 @@ const make = (() => {
center: page.center,
},
shouldAnimate: true,
+ isScrollAllowed: true,
};
const state: DragState = {
diff --git a/test/unit/view/connected-droppable-dimension-publisher.spec.js b/test/unit/view/connected-droppable-dimension-publisher.spec.js
index 2aa874e986..79d9b9f6f3 100644
--- a/test/unit/view/connected-droppable-dimension-publisher.spec.js
+++ b/test/unit/view/connected-droppable-dimension-publisher.spec.js
@@ -10,7 +10,7 @@ const shouldPublishMapProps: MapProps = {
shouldPublish: true,
};
-describe('Dimension publisher - connected', () => {
+describe('Connected droppable dimension publisher', () => {
it('should return the default props when not requested to publish dimensions', () => {
const selector = makeSelector();
diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js
index 3eed72be00..6d2c6f899b 100644
--- a/test/unit/view/connected-droppable.spec.js
+++ b/test/unit/view/connected-droppable.spec.js
@@ -6,7 +6,7 @@ import Droppable, { makeSelector } from '../../../src/view/droppable/connected-d
import noImpact from '../../../src/state/no-impact';
import { getDraggableDimension } from '../../../src/state/dimension';
import { withStore } from '../../utils/get-context-options';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import type {
Phase,
DragState,
@@ -19,64 +19,79 @@ import type {
DropResult,
CurrentDrag,
DraggableDimension,
+ DraggableLocation,
InitialDragLocation,
CurrentDragLocation,
} from '../../../src/types';
-import type { MapProps, Provided, Selector } from '../../../src/view/droppable/droppable-types';
+import type {
+ MapProps,
+ Provided,
+ Selector,
+ Placeholder,
+} from '../../../src/view/droppable/droppable-types';
type ExecuteArgs = {|
- id: DroppableId,
+ droppableId: DroppableId,
phase: Phase,
drag: ?DragState,
pending: ?PendingDrop,
- isDropDisabled?: boolean
+ draggable: ?DraggableDimension,
+ // being simple and defaulting to false (droppable is enabled)
+ isDropDisabled?: boolean,
|}
const execute = (selector: Selector) =>
- ({ phase, drag, pending, id, isDropDisabled = false }: ExecuteArgs) =>
+ ({ phase, drag, draggable, pending, droppableId, isDropDisabled = false }: ExecuteArgs) =>
selector.resultFunc(
- phase, drag, pending, id, isDropDisabled,
+ phase, drag, draggable, pending, droppableId, isDropDisabled,
);
const defaultMapProps: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
-const droppableId: DroppableId = 'drop-1';
+const homeDroppableId: DroppableId = 'home-id';
+const foreignDroppableId: DroppableId = 'foreign-droppable';
const draggableId: DraggableId = 'drag-1';
const origin: Position = { x: 0, y: 0 };
type DragArgs = {|
- isDraggingOver: boolean
+ isDraggingOver: false | 'home' | 'foreign'
|}
-const perform = (() => {
- const dimension: DraggableDimension = getDraggableDimension({
- id: draggableId,
- droppableId,
- clientRect: getClientRect({
- top: 100,
- left: 0,
- right: 100,
- bottom: 200,
- }),
- });
+const draggable: DraggableDimension = getDraggableDimension({
+ id: draggableId,
+ droppableId: homeDroppableId,
+ clientRect: getClientRect({
+ top: 100,
+ left: 0,
+ right: 100,
+ bottom: 200,
+ }),
+});
+const placeholder: Placeholder = {
+ width: draggable.page.withoutMargin.width,
+ height: draggable.page.withoutMargin.height,
+};
+
+const perform = (() => {
const initial: InitialDrag = (() => {
const client: InitialDragLocation = {
- selection: dimension.client.withoutMargin.center,
- center: dimension.client.withoutMargin.center,
+ selection: draggable.client.withoutMargin.center,
+ center: draggable.client.withoutMargin.center,
};
const page: InitialDragLocation = {
- selection: dimension.page.withoutMargin.center,
- center: dimension.page.withoutMargin.center,
+ selection: draggable.page.withoutMargin.center,
+ center: draggable.page.withoutMargin.center,
};
const value: InitialDrag = {
source: {
index: 0,
- droppableId,
+ droppableId: homeDroppableId,
},
client,
page,
@@ -85,6 +100,7 @@ const perform = (() => {
center: page.center,
},
};
+
return value;
})();
@@ -111,30 +127,59 @@ const perform = (() => {
center: page.center,
},
shouldAnimate: true,
+ isScrollAllowed: true,
};
return value;
})();
- const dragOverImpact: DragImpact = {
+ const homeDestination: DraggableLocation = {
+ index: initial.source.index + 1,
+ droppableId: homeDroppableId,
+ };
+ const foreignDestination: DraggableLocation = {
+ index: 0,
+ droppableId: foreignDroppableId,
+ };
+
+ const dragOverHomeImpact: DragImpact = {
movement: {
draggables: [draggableId],
amount: {
- y: dimension.page.withMargin.height,
+ y: draggable.page.withMargin.height,
x: 0,
},
isBeyondStartPosition: true,
},
direction: 'vertical',
- destination: {
- index: initial.source.index + 1,
- droppableId,
+ destination: homeDestination,
+ };
+
+ const dragOverForeignImpact: DragImpact = {
+ movement: {
+ draggables: [],
+ amount: {
+ y: draggable.page.withMargin.height,
+ x: 0,
+ },
+ isBeyondStartPosition: false,
},
+ direction: 'vertical',
+ destination: foreignDestination,
};
const drag = ({ isDraggingOver }: DragArgs): DragState => {
+ const impact: DragImpact = (() => {
+ if (isDraggingOver === 'home') {
+ return dragOverHomeImpact;
+ }
+ if (isDraggingOver === 'foreign') {
+ return dragOverForeignImpact;
+ }
+ return noImpact;
+ })();
const state: DragState = {
current,
- impact: isDraggingOver ? dragOverImpact : noImpact,
+ impact,
initial,
};
return state;
@@ -142,25 +187,34 @@ const perform = (() => {
const drop = ({ isDraggingOver }: DragArgs): PendingDrop => {
// some made up position
- const newHomeOffset: Position = {
+ const dontCare: Position = {
x: 100,
y: 20,
};
+ const destination: ?DraggableLocation = (() => {
+ if (isDraggingOver === 'home') {
+ return homeDestination;
+ }
+ if (isDraggingOver === 'foreign') {
+ return foreignDestination;
+ }
+ return null;
+ })();
+
+ const impact: DragImpact = drag({ isDraggingOver }).impact;
+
const result: DropResult = {
draggableId,
type: 'TYPE',
source: initial.source,
- destination: {
- index: initial.source.index + 1,
- droppableId: initial.source.droppableId,
- },
+ destination,
};
const pending: PendingDrop = {
trigger: 'DROP',
- newHomeOffset,
- impact: isDraggingOver ? dragOverImpact : noImpact,
+ newHomeOffset: dontCare,
+ impact,
result,
};
@@ -189,7 +243,8 @@ describe('Droppable - connected', () => {
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
isDropDisabled: true,
});
@@ -205,19 +260,49 @@ describe('Droppable - connected', () => {
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
+ isDropDisabled: true,
});
const second: MapProps = execute(selector)({
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
+ isDropDisabled: true,
});
// checking object equality
expect(first).toBe(second);
});
});
+
+ it('should not break memoization between phases', () => {
+ let previous: MapProps;
+ const selector = makeSelector();
+
+ phases.forEach((phase: Phase) => {
+ const result: MapProps = execute(selector)({
+ phase,
+ drag: null,
+ pending: null,
+ draggable: null,
+ droppableId: homeDroppableId,
+ isDropDisabled: true,
+ });
+
+ // seed previous
+ if (!previous) {
+ previous = result;
+ return;
+ }
+
+ // checking object equality
+ expect(result).toBe(previous);
+ expect(result).toEqual(defaultMapProps);
+ });
+ });
});
describe('while dragging', () => {
@@ -225,25 +310,28 @@ describe('Droppable - connected', () => {
const props: MapProps = execute(makeSelector())({
phase: 'DRAGGING',
drag: null,
+ draggable: null,
pending: null,
- id: droppableId,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(defaultMapProps);
expect(console.error).toHaveBeenCalled();
});
- describe('dragging over', () => {
+ describe('over home droppable', () => {
it('should return that it is dragging over', () => {
const expected: MapProps = {
isDraggingOver: true,
+ placeholder: null,
};
const props: MapProps = execute(makeSelector())({
phase: 'DRAGGING',
- drag: perform.drag({ isDraggingOver: true }),
+ drag: perform.drag({ isDraggingOver: 'home' }),
+ draggable,
pending: null,
- id: droppableId,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(expected);
@@ -253,19 +341,22 @@ describe('Droppable - connected', () => {
const selector = makeSelector();
const expected: MapProps = {
isDraggingOver: true,
+ placeholder: null,
};
const props1: MapProps = execute(selector)({
phase: 'DRAGGING',
- drag: perform.drag({ isDraggingOver: true }),
+ drag: perform.drag({ isDraggingOver: 'home' }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
const props2: MapProps = execute(selector)({
phase: 'DRAGGING',
- drag: perform.drag({ isDraggingOver: true }),
+ drag: perform.drag({ isDraggingOver: 'home' }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
// checking object equality
@@ -275,17 +366,67 @@ describe('Droppable - connected', () => {
});
});
+ describe('over foreign droppable', () => {
+ it('should return that it is dragging over and a placeholder', () => {
+ const expected: MapProps = {
+ isDraggingOver: true,
+ placeholder,
+ };
+
+ const props: MapProps = execute(makeSelector())({
+ phase: 'DRAGGING',
+ drag: perform.drag({ isDraggingOver: 'foreign' }),
+ draggable,
+ pending: null,
+ droppableId: foreignDroppableId,
+ });
+
+ expect(props).toEqual(expected);
+ });
+
+ it('should not break memoization on multiple drags', () => {
+ const selector = makeSelector();
+ const expected: MapProps = {
+ isDraggingOver: true,
+ placeholder,
+ };
+
+ const props1: MapProps = execute(selector)({
+ phase: 'DRAGGING',
+ drag: perform.drag({ isDraggingOver: 'foreign' }),
+ pending: null,
+ draggable,
+ droppableId: foreignDroppableId,
+ });
+ const props2: MapProps = execute(selector)({
+ phase: 'DRAGGING',
+ drag: perform.drag({ isDraggingOver: 'foreign' }),
+ pending: null,
+ draggable,
+ droppableId: foreignDroppableId,
+ });
+
+ // checking object equality
+ expect(props1).toBe(props2);
+ expect(props1.placeholder).toBe(props2.placeholder);
+ expect(props1).toEqual(expected);
+ expect(props2).toEqual(expected);
+ });
+ });
+
describe('not dragging over', () => {
it('should return that it is not dragging over', () => {
const expected: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
const props: MapProps = execute(makeSelector())({
phase: 'DRAGGING',
drag: perform.drag({ isDraggingOver: false }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(expected);
@@ -295,19 +436,22 @@ describe('Droppable - connected', () => {
const selector = makeSelector();
const expected: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
const props1: MapProps = execute(selector)({
phase: 'DRAGGING',
drag: perform.drag({ isDraggingOver: false }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
const props2: MapProps = execute(selector)({
phase: 'DRAGGING',
drag: perform.drag({ isDraggingOver: false }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
// checking object equality
@@ -324,24 +468,74 @@ describe('Droppable - connected', () => {
phase: 'DROP_ANIMATING',
drag: null,
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(defaultMapProps);
expect(console.error).toHaveBeenCalled();
});
- describe('dragging over', () => {
+ describe('was dragging over home droppable', () => {
it('should return that it is dragging over', () => {
const expected: MapProps = {
isDraggingOver: true,
+ placeholder: null,
+ };
+
+ const props: MapProps = execute(makeSelector())({
+ phase: 'DROP_ANIMATING',
+ drag: null,
+ draggable,
+ pending: perform.drop({ isDraggingOver: 'home' }),
+ droppableId: homeDroppableId,
+ });
+
+ expect(props).toEqual(expected);
+ });
+
+ it('should not break memoization from a previous DRAGGING phase', () => {
+ const selector = makeSelector();
+ const expected: MapProps = {
+ isDraggingOver: true,
+ placeholder: null,
+ };
+
+ const dragging: MapProps = execute(selector)({
+ phase: 'DRAGGING',
+ drag: perform.drag({ isDraggingOver: 'home' }),
+ pending: null,
+ draggable,
+ droppableId: homeDroppableId,
+ });
+ const dropAnimating: MapProps = execute(selector)({
+ phase: 'DROP_ANIMATING',
+ drag: null,
+ pending: perform.drop({ isDraggingOver: 'home' }),
+ draggable,
+ droppableId: homeDroppableId,
+ });
+
+ expect(dragging).toEqual(expected);
+ expect(dropAnimating).toEqual(expected);
+ // checking object equality
+ expect(dragging).toBe(dropAnimating);
+ });
+ });
+
+ describe('was dragging over foreign droppable', () => {
+ it('should return that it is dragging over and provide a placeholder', () => {
+ const expected: MapProps = {
+ isDraggingOver: true,
+ placeholder,
};
const props: MapProps = execute(makeSelector())({
phase: 'DROP_ANIMATING',
drag: null,
- pending: perform.drop({ isDraggingOver: true }),
- id: droppableId,
+ pending: perform.drop({ isDraggingOver: 'foreign' }),
+ draggable,
+ droppableId: foreignDroppableId,
});
expect(props).toEqual(expected);
@@ -351,19 +545,22 @@ describe('Droppable - connected', () => {
const selector = makeSelector();
const expected: MapProps = {
isDraggingOver: true,
+ placeholder,
};
const dragging: MapProps = execute(selector)({
phase: 'DRAGGING',
- drag: perform.drag({ isDraggingOver: true }),
+ drag: perform.drag({ isDraggingOver: 'foreign' }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: foreignDroppableId,
});
const dropAnimating: MapProps = execute(selector)({
phase: 'DROP_ANIMATING',
drag: null,
- pending: perform.drop({ isDraggingOver: true }),
- id: droppableId,
+ pending: perform.drop({ isDraggingOver: 'foreign' }),
+ draggable,
+ droppableId: foreignDroppableId,
});
expect(dragging).toEqual(expected);
@@ -377,13 +574,15 @@ describe('Droppable - connected', () => {
it('should return that it is not dragging over', () => {
const expected: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
const props: MapProps = execute(makeSelector())({
phase: 'DROP_ANIMATING',
drag: null,
+ draggable,
pending: perform.drop({ isDraggingOver: false }),
- id: droppableId,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(expected);
@@ -393,19 +592,22 @@ describe('Droppable - connected', () => {
const selector = makeSelector();
const expected: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
const dragging: MapProps = execute(selector)({
phase: 'DRAGGING',
drag: perform.drag({ isDraggingOver: false }),
pending: null,
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
const dropAnimating: MapProps = execute(selector)({
phase: 'DROP_ANIMATING',
drag: null,
pending: perform.drop({ isDraggingOver: false }),
- id: droppableId,
+ draggable,
+ droppableId: homeDroppableId,
});
expect(dragging).toEqual(expected);
@@ -417,17 +619,18 @@ describe('Droppable - connected', () => {
});
describe('other phases', () => {
- const other: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DROP_COMPLETE'];
+ const others: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DROP_COMPLETE'];
it('should return the default props', () => {
const selector = makeSelector();
- other.forEach((phase: Phase): void => {
+ others.forEach((phase: Phase): void => {
const props: MapProps = execute(selector)({
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
});
expect(props).toEqual(defaultMapProps);
@@ -437,18 +640,20 @@ describe('Droppable - connected', () => {
it('should not break memoization on multiple calls', () => {
const selector = makeSelector();
- other.forEach((phase: Phase): void => {
+ others.forEach((phase: Phase): void => {
const first: MapProps = execute(selector)({
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
});
const second: MapProps = execute(selector)({
phase,
drag: null,
pending: null,
- id: droppableId,
+ draggable: null,
+ droppableId: homeDroppableId,
});
expect(first).toEqual(defaultMapProps);
diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js
index f067487337..d34f534e0c 100644
--- a/test/unit/view/drag-handle.spec.js
+++ b/test/unit/view/drag-handle.spec.js
@@ -21,6 +21,8 @@ const getStubCallbacks = (): Callbacks => ({
onMove: jest.fn(),
onMoveForward: jest.fn(),
onMoveBackward: jest.fn(),
+ onCrossAxisMoveForward: jest.fn(),
+ onCrossAxisMoveBackward: jest.fn(),
onDrop: jest.fn(),
onCancel: jest.fn(),
onWindowScroll: jest.fn(),
@@ -32,6 +34,8 @@ type CallBacksCalledFn = {|
onMove?: number,
onMoveForward?: number,
onMoveBackward?: number,
+ onCrossAxisMoveForward ?: number,
+ onCrossAxisMoveBackward?: number,
onDrop?: number,
onCancel ?: number,
onWindowScroll ?: number,
@@ -43,6 +47,8 @@ const callbacksCalled = (callbacks: Callbacks) => ({
onMove = 0,
onMoveForward = 0,
onMoveBackward = 0,
+ onCrossAxisMoveForward = 0,
+ onCrossAxisMoveBackward = 0,
onDrop = 0,
onCancel = 0,
}: CallBacksCalledFn = {}) =>
@@ -52,7 +58,9 @@ const callbacksCalled = (callbacks: Callbacks) => ({
callbacks.onMoveForward.mock.calls.length === onMoveForward &&
callbacks.onMoveBackward.mock.calls.length === onMoveBackward &&
callbacks.onDrop.mock.calls.length === onDrop &&
- callbacks.onCancel.mock.calls.length === onCancel;
+ callbacks.onCancel.mock.calls.length === onCancel &&
+ callbacks.onCrossAxisMoveForward.mock.calls.length === onCrossAxisMoveForward &&
+ callbacks.onCrossAxisMoveBackward.mock.calls.length === onCrossAxisMoveBackward;
const whereAnyCallbacksCalled = (callbacks: Callbacks) =>
!callbacksCalled(callbacks)();
@@ -1104,25 +1112,25 @@ describe('drag handle', () => {
})).toBe(true);
});
- it('should not move backward when the user presses LeftArrow', () => {
+ it('should request to move to a droppable on the left when the user presses LeftArrow', () => {
pressSpacebar(wrapper);
windowArrowLeft();
requestAnimationFrame.step();
expect(callbacksCalled(callbacks)({
onKeyLift: 1,
- onMoveBackward: 0,
+ onCrossAxisMoveBackward: 1,
})).toBe(true);
});
- it('should not move forward when the user presses RightArrow', () => {
+ it('should request to move to a droppable on the right when the user presses RightArrow', () => {
pressSpacebar(wrapper);
windowArrowRight();
requestAnimationFrame.step();
expect(callbacksCalled(callbacks)({
onKeyLift: 1,
- onMoveForward: 0,
+ onCrossAxisMoveForward: 1,
})).toBe(true);
});
});
@@ -1152,49 +1160,47 @@ describe('drag handle', () => {
customWrapper.unmount();
});
- it('should not move backward when the user presses ArrowUp', () => {
+ it('should move backward when the user presses LeftArrow', () => {
pressSpacebar(customWrapper);
- // try move backward
- windowArrowUp();
+ windowArrowLeft();
requestAnimationFrame.step();
expect(callbacksCalled(customCallbacks)({
onKeyLift: 1,
- onMoveBackward: 0,
+ onMoveBackward: 1,
})).toBe(true);
});
- it('should not move forward when the user presses ArrowDown', () => {
+ it('should move forward when the user presses RightArrow', () => {
pressSpacebar(customWrapper);
- // try move forward
- windowArrowDown();
+ windowArrowRight();
requestAnimationFrame.step();
expect(callbacksCalled(customCallbacks)({
onKeyLift: 1,
- onMoveForward: 0,
+ onMoveForward: 1,
})).toBe(true);
});
- it('should move backward when the user presses LeftArrow', () => {
+ it('should request a backward cross axis move when the user presses ArrowUp', () => {
pressSpacebar(customWrapper);
- windowArrowLeft();
+ windowArrowUp();
requestAnimationFrame.step();
expect(callbacksCalled(customCallbacks)({
onKeyLift: 1,
- onMoveBackward: 1,
+ onCrossAxisMoveBackward: 1,
})).toBe(true);
});
- it('should move forward when the user presses RightArrow', () => {
+ it('should request a forward cross axis move when the user presses ArrowDown', () => {
pressSpacebar(customWrapper);
- windowArrowRight();
+ windowArrowDown();
requestAnimationFrame.step();
expect(callbacksCalled(customCallbacks)({
onKeyLift: 1,
- onMoveForward: 1,
+ onCrossAxisMoveForward: 1,
})).toBe(true);
});
});
diff --git a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js
index e5ba43f1b5..a3930635ab 100644
--- a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js
+++ b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js
@@ -3,11 +3,11 @@ import React, { Component } from 'react';
import { mount } from 'enzyme';
import DraggableDimensionPublisher from '../../../src/view/draggable-dimension-publisher/draggable-dimension-publisher';
import { getDraggableDimension } from '../../../src/state/dimension';
-// eslint-disable-next-line no-duplicate-imports
-import type { ClientRect, Margin } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import setWindowScroll from '../../utils/set-window-scroll';
import type {
+ Spacing,
+ ClientRect,
Position,
DraggableId,
DroppableId,
@@ -28,11 +28,15 @@ const dimension: DraggableDimension = getDraggableDimension({
}),
});
-const noComputedMargin = {
+const noSpacing = {
marginTop: '0',
marginRight: '0',
marginBottom: '0',
marginLeft: '0',
+ paddingTop: '0',
+ paddingRight: '0',
+ paddingBottom: '0',
+ paddingLeft: '0',
};
class Item extends Component {
@@ -105,7 +109,7 @@ describe('DraggableDimensionPublisher', () => {
height: dimension.page.withoutMargin.height,
width: dimension.page.withoutMargin.width,
}));
- jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noComputedMargin);
+ jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
const wrapper = mount(
);
wrapper.setProps({
@@ -117,7 +121,7 @@ describe('DraggableDimensionPublisher', () => {
});
it('should consider any margins when calculating dimensions', () => {
- const margin: Margin = {
+ const margin: Spacing = {
top: 10,
right: 30,
bottom: 40,
@@ -181,7 +185,7 @@ describe('DraggableDimensionPublisher', () => {
windowScroll,
});
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => clientRect);
- jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noComputedMargin);
+ jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
setWindowScroll(windowScroll);
const wrapper = mount(
);
diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js
index f151e38204..8372719222 100644
--- a/test/unit/view/unconnected-draggable.spec.js
+++ b/test/unit/view/unconnected-draggable.spec.js
@@ -7,7 +7,7 @@ import type { ReactWrapper } from 'enzyme';
import Draggable, { zIndexOptions } from '../../../src/view/draggable/draggable';
import DragHandle, { sloppyClickThreshold } from '../../../src/view/drag-handle/drag-handle';
import Moveable from '../../../src/view/moveable/';
-import Placeholder from '../../../src/view/draggable/placeholder';
+import Placeholder from '../../../src/view/placeholder';
import { css } from '../../../src/view/animation';
import { add, subtract } from '../../../src/state/position';
import type {
@@ -28,7 +28,7 @@ import type {
InitialDragLocation,
} from '../../../src/types';
import { getDraggableDimension } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import { combine, withStore, withDroppableId } from '../../utils/get-context-options';
import { dispatchWindowMouseEvent, mouseEvent } from '../../utils/user-input-util';
import setWindowScroll from '../../utils/set-window-scroll';
@@ -77,6 +77,8 @@ const getDispatchPropsStub = (): DispatchProps => ({
moveByWindowScroll: jest.fn(),
moveForward: jest.fn(),
moveBackward: jest.fn(),
+ crossAxisMoveForward: jest.fn(),
+ crossAxisMoveBackward: jest.fn(),
drop: jest.fn(),
cancel: jest.fn(),
dropAnimationFinished: jest.fn(),
@@ -712,6 +714,84 @@ describe('Draggable - unconnected', () => {
});
});
+ describe('onCrossAxisMoveForward', () => {
+ it('should throw if dragging is disabled', () => {
+ const wrapper = mountDraggable({
+ ownProps: disabledOwnProps,
+ mapProps: draggingMapProps,
+ });
+
+ const tryMove = () =>
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId);
+
+ expect(tryMove).toThrow();
+ });
+
+ it('should throw if not attached to the DOM', () => {
+ const wrapper = mountDraggable({
+ mapProps: draggingMapProps,
+ });
+
+ wrapper.unmount();
+
+ const tryMove = () =>
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId);
+
+ expect(tryMove).toThrow();
+ });
+
+ it('should call the cross axis move forward action', () => {
+ const dispatchProps = getDispatchPropsStub();
+ const wrapper = mountDraggable({
+ mapProps: draggingMapProps,
+ dispatchProps,
+ });
+
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId);
+
+ expect(dispatchProps.crossAxisMoveForward).toBeCalledWith(draggableId);
+ });
+ });
+
+ describe('onCrossAxisMoveBackward', () => {
+ it('should throw if dragging is disabled', () => {
+ const wrapper = mountDraggable({
+ ownProps: disabledOwnProps,
+ mapProps: draggingMapProps,
+ });
+
+ const tryMove = () =>
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId);
+
+ expect(tryMove).toThrow();
+ });
+
+ it('should throw if not attached to the DOM', () => {
+ const wrapper = mountDraggable({
+ mapProps: draggingMapProps,
+ });
+
+ wrapper.unmount();
+
+ const tryMove = () =>
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId);
+
+ expect(tryMove).toThrow();
+ });
+
+ it('should call the move cross axis backwards action', () => {
+ const dispatchProps = getDispatchPropsStub();
+ const wrapper = mountDraggable({
+ mapProps: draggingMapProps,
+ dispatchProps,
+ });
+
+ wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId);
+
+ expect(dispatchProps.crossAxisMoveBackward).toBeCalledWith(draggableId);
+ });
+ });
+
describe('onCancel', () => {
it('should call the cancel dispatch prop', () => {
const dispatchProps = getDispatchPropsStub();
diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js
index b2ae39ba61..4057ac1f9b 100644
--- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js
+++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js
@@ -1,21 +1,23 @@
// @flow
+/* eslint-disable react/no-multi-comp */
import React, { Component } from 'react';
import { mount } from 'enzyme';
import DroppableDimensionPublisher from '../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher';
import { getDroppableDimension } from '../../../src/state/dimension';
-// eslint-disable-next-line no-duplicate-imports
-import type { Margin, ClientRect } from '../../../src/state/dimension';
-import getClientRect from '../../utils/get-client-rect';
+import getClientRect from '../../../src/state/get-client-rect';
import setWindowScroll from '../../utils/set-window-scroll';
import type {
+ ClientRect,
+ Spacing,
DroppableId,
DroppableDimension,
HTMLElement,
Position,
+ ReactElement,
} from '../../../src/types';
const droppableId: DroppableId = 'drop-1';
-const dimension: DroppableDimension = getDroppableDimension({
+const droppable: DroppableDimension = getDroppableDimension({
id: droppableId,
clientRect: getClientRect({
top: 0,
@@ -24,13 +26,37 @@ const dimension: DroppableDimension = getDroppableDimension({
left: 0,
}),
});
+const origin: Position = { x: 0, y: 0 };
+
+const noMargin = {
+ marginTop: '0',
+ marginRight: '0',
+ marginBottom: '0',
+ marginLeft: '0',
+};
+const noPadding = {
+ paddingTop: '0',
+ paddingRight: '0',
+ paddingBottom: '0',
+ paddingLeft: '0',
+};
+
+const noSpacing = {
+ ...noMargin,
+ ...noPadding,
+};
class ScrollableItem extends Component {
/* eslint-disable react/sort-comp */
props: {
+ // dispatch props
publish: (dimension: DroppableDimension) => void,
updateScroll: (id: DroppableId, offset: Position) => void,
+ updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void,
+ // map props (default: false)
shouldPublish?: boolean,
+ // scrollable item prop (default: false)
+ isDropDisabled?: boolean,
}
state: {|
@@ -49,13 +75,15 @@ class ScrollableItem extends Component {
render() {
return (
- // $ExpectError - for an unknown reason flow is having a hard time with this
{
x: window.pageXOffset,
y: window.pageYOffset,
};
+ let publish;
+ let updateScroll;
+ let updateIsEnabled;
+ let dispatchProps;
+ let wrapper;
+
+ beforeEach(() => {
+ publish = jest.fn();
+ updateScroll = jest.fn();
+ updateIsEnabled = jest.fn();
+ dispatchProps = {
+ publish, updateScroll, updateIsEnabled,
+ };
+ });
afterEach(() => {
// clean up any stubs
@@ -85,18 +127,16 @@ describe('DraggableDimensionPublisher', () => {
window.getComputedStyle.mockRestore();
}
setWindowScroll(originalWindowScroll, { shouldPublish: false });
+
+ if (wrapper) {
+ wrapper.unmount();
+ }
});
describe('dimension publishing', () => {
it('should not publish if not asked to', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
-
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
@@ -105,53 +145,38 @@ describe('DraggableDimensionPublisher', () => {
expect(publish).not.toHaveBeenCalled();
expect(updateScroll).not.toHaveBeenCalled();
-
- wrapper.unmount();
+ expect(updateIsEnabled).not.toHaveBeenCalled();
});
it('should publish the dimensions of the target', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- top: dimension.page.withoutMargin.top,
- bottom: dimension.page.withoutMargin.bottom,
- left: dimension.page.withoutMargin.left,
- right: dimension.page.withoutMargin.right,
- height: dimension.page.withoutMargin.height,
- width: dimension.page.withoutMargin.width,
- }));
- jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
- marginTop: '0',
- marginRight: '0',
- marginBottom: '0',
- marginLeft: '0',
+ top: droppable.page.withoutMargin.top,
+ bottom: droppable.page.withoutMargin.bottom,
+ left: droppable.page.withoutMargin.left,
+ right: droppable.page.withoutMargin.right,
+ height: droppable.page.withoutMargin.height,
+ width: droppable.page.withoutMargin.width,
}));
+ jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
});
- expect(publish).toBeCalledWith(dimension);
+ expect(publish).toBeCalledWith(droppable);
expect(publish).toHaveBeenCalledTimes(1);
-
- wrapper.unmount();
});
it('should consider any margins when calculating dimensions', () => {
- const margin: Margin = {
+ const margin: Spacing = {
top: 10,
right: 30,
bottom: 40,
left: 50,
};
- const publish = jest.fn();
- const updateScroll = jest.fn();
const expected: DroppableDimension = getDroppableDimension({
id: droppableId,
clientRect: getClientRect({
@@ -163,38 +188,32 @@ describe('DraggableDimensionPublisher', () => {
margin,
});
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- top: dimension.page.withoutMargin.top,
- bottom: dimension.page.withoutMargin.bottom,
- left: dimension.page.withoutMargin.left,
- right: dimension.page.withoutMargin.right,
- height: dimension.page.withoutMargin.height,
- width: dimension.page.withoutMargin.width,
+ top: droppable.page.withoutMargin.top,
+ bottom: droppable.page.withoutMargin.bottom,
+ left: droppable.page.withoutMargin.left,
+ right: droppable.page.withoutMargin.right,
+ height: droppable.page.withoutMargin.height,
+ width: droppable.page.withoutMargin.width,
}));
jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
marginTop: `${margin.top}`,
marginRight: `${margin.right}`,
marginBottom: `${margin.bottom}`,
marginLeft: `${margin.left}`,
+ ...noPadding,
}));
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
});
expect(publish).toBeCalledWith(expected);
-
- wrapper.unmount();
});
it('should consider the window scroll when calculating dimensions', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
const windowScroll: Position = {
x: 500,
y: 1000,
@@ -212,17 +231,9 @@ describe('DraggableDimensionPublisher', () => {
windowScroll,
});
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => clientRect);
- jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
- marginTop: '0',
- marginRight: '0',
- marginBottom: '0',
- marginLeft: '0',
- }));
- const wrapper = mount(
-
,
+ jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
+ wrapper = mount(
+
,
);
wrapper.setProps({
@@ -230,13 +241,9 @@ describe('DraggableDimensionPublisher', () => {
});
expect(publish).toHaveBeenCalledWith(expected);
-
- wrapper.unmount();
});
it('should consider the closest scrollable when calculating dimensions', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
const closestScroll: Position = {
x: 500,
y: 1000,
@@ -261,16 +268,10 @@ describe('DraggableDimensionPublisher', () => {
}));
jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
overflowY: 'scroll',
- marginTop: '0',
- marginRight: '0',
- marginBottom: '0',
- marginLeft: '0',
+ ...noSpacing,
}));
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
// setting initial scroll
const container: HTMLElement = wrapper.getDOMNode();
@@ -282,28 +283,21 @@ describe('DraggableDimensionPublisher', () => {
});
expect(publish).toHaveBeenCalledWith(expected);
-
- wrapper.unmount();
});
it('should not publish unless it is freshly required to do so', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- top: dimension.page.withMargin.top,
- bottom: dimension.page.withMargin.bottom,
- left: dimension.page.withMargin.left,
- right: dimension.page.withMargin.right,
- height: dimension.page.withMargin.height,
- width: dimension.page.withMargin.width,
+ top: droppable.page.withMargin.top,
+ bottom: droppable.page.withMargin.bottom,
+ left: droppable.page.withMargin.left,
+ right: droppable.page.withMargin.right,
+ height: droppable.page.withMargin.height,
+ width: droppable.page.withMargin.width,
}));
// initial publish
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -327,20 +321,369 @@ describe('DraggableDimensionPublisher', () => {
// just being extra safe:
expect(updateScroll).not.toHaveBeenCalled();
+ });
- wrapper.unmount();
+ describe('dimension clipping', () => {
+ type ItemProps = {
+ publish: (dimension: DroppableDimension) => void,
+ updateScroll: (id: DroppableId, offset: Position) => void,
+ updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void,
+ shouldPublish?: boolean,
+ };
+
+ class ScrollParent extends Component {
+ props: {
+ children: ?ReactElement
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+ }
+
+ class Item extends Component {
+ /* eslint-disable react/sort-comp */
+ props: ItemProps
+
+ state: {|
+ ref: ?HTMLElement
+ |}
+
+ state = {
+ ref: null,
+ }
+
+ setRef = (ref: ?HTMLElement) => {
+ this.setState({
+ ref,
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ class App extends Component {
+ props: ItemProps
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ type ExecuteArgs = {|
+ droppableRect: ClientRect,
+ scrollParentRect: ClientRect,
+ scrollParentScroll?: Position,
+ |}
+
+ const execute = ({
+ droppableRect,
+ scrollParentRect,
+ scrollParentScroll = origin,
+ }: ExecuteArgs) => {
+ wrapper = mount(
+
+ );
+
+ const scrollParentNode: HTMLElement = wrapper.getDOMNode();
+
+ if (!scrollParentNode.classList.contains('scroll-parent')) {
+ throw new Error('scroll parent node not obtained correctly');
+ }
+
+ const droppableNode: HTMLElement = scrollParentNode.querySelector('.item');
+
+ scrollParentNode.scrollLeft = scrollParentScroll.x;
+ scrollParentNode.scrollTop = scrollParentScroll.y;
+
+ jest.spyOn(scrollParentNode, 'getBoundingClientRect').mockImplementationOnce(() => scrollParentRect);
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementationOnce(() => droppableRect);
+
+ jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
+ if (el === droppableNode) {
+ return noSpacing;
+ }
+
+ if (el === scrollParentNode) {
+ return {
+ ...noSpacing,
+ overflow: 'auto',
+ };
+ }
+
+ throw new Error('unknown el');
+ });
+
+ wrapper.setProps({
+ shouldPublish: true,
+ });
+ };
+
+ it('should clip the dimension by the size of the scroll parent', () => {
+ const droppableRect: ClientRect = getClientRect({
+ top: 0,
+ bottom: 100,
+ left: 0,
+ right: 100,
+ });
+ // smaller by 10px in every direction
+ const scrollParentRect: ClientRect = getClientRect({
+ top: 10,
+ bottom: 90,
+ left: 10,
+ right: 90,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ // because it is smaller in every direction
+ // the result will be the scroll parent rect
+ clientRect: scrollParentRect,
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ describe('dimension clipping by edge', () => {
+ const base = {
+ top: 0,
+ bottom: 100,
+ left: 0,
+ right: 100,
+ };
+ const droppableRect: ClientRect = getClientRect(base);
+
+ describe('cut off by scroll container', () => {
+ it('should choose the biggest top value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ top: 10,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect({
+ ...base,
+ top: 10,
+ }),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the biggest left value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ left: 10,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect({
+ ...base,
+ left: 10,
+ }),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the smallest right value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ right: 90,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect({
+ ...base,
+ right: 90,
+ }),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the smallest bottom value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ bottom: 90,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect({
+ ...base,
+ bottom: 90,
+ }),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('cut off by droppable rect', () => {
+ it('should choose the biggest top value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ top: -10,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect(base),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the biggest left value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ left: -10,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect(base),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the smallest right value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ right: 110,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect(base),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+
+ it('should choose the smallest bottom value', () => {
+ const scrollParentRect: ClientRect = getClientRect({
+ ...base,
+ bottom: 110,
+ });
+ const expected = getDroppableDimension({
+ id: droppableId,
+ clientRect: getClientRect(base),
+ });
+
+ execute({ droppableRect, scrollParentRect });
+
+ // the trimmed rect
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('should take into account the parents scroll when clipping', () => {
+ const base = {
+ top: 0,
+ bottom: 100,
+ left: 0,
+ right: 100,
+ };
+ const scrollParentScroll: Position = {
+ x: 100,
+ y: 100,
+ };
+ // rect will have the scroll subtracted when measurements are taken
+ const droppableRect: ClientRect = getClientRect({
+ top: base.top - scrollParentScroll.y,
+ bottom: base.bottom - scrollParentScroll.y,
+ left: base.left - scrollParentScroll.x,
+ right: base.right - scrollParentScroll.x,
+ });
+ const scrollParentRect: ClientRect = getClientRect(base);
+ const expected: DroppableDimension = getDroppableDimension({
+ id: droppableId,
+ scroll: scrollParentScroll,
+ clientRect: getClientRect(base),
+ });
+
+ execute({ droppableRect, scrollParentRect, scrollParentScroll });
+
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
});
});
describe('scroll watching', () => {
it('should not publish any scroll changes unless told it can publish', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
const container: HTMLElement = wrapper.getDOMNode();
@@ -358,18 +701,11 @@ describe('DraggableDimensionPublisher', () => {
requestAnimationFrame.flush();
expect(updateScroll).not.toHaveBeenCalled();
-
- wrapper.unmount();
});
it('should publish the closest scrollable scroll offset', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -389,18 +725,11 @@ describe('DraggableDimensionPublisher', () => {
expect(updateScroll.mock.calls[0]).toEqual([
droppableId, { x: 500, y: 1000 },
]);
-
- wrapper.unmount();
});
it('should throttle multiple scrolls into a animation frame', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -428,19 +757,12 @@ describe('DraggableDimensionPublisher', () => {
// also checking that no loose frames are stored up
requestAnimationFrame.flush();
expect(updateScroll).toHaveBeenCalledTimes(1);
-
- wrapper.unmount();
});
it('should not fire a scroll if the value has not changed since the previous frame', () => {
// this can happen if you scroll backward and forward super quick
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -470,19 +792,12 @@ describe('DraggableDimensionPublisher', () => {
requestAnimationFrame.step();
expect(updateScroll).toHaveBeenCalledTimes(1);
-
- wrapper.unmount();
});
it('should stop watching scroll when no longer required to publish', () => {
// this can happen if you scroll backward and forward super quick
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -507,18 +822,11 @@ describe('DraggableDimensionPublisher', () => {
// let any frames go that want to
requestAnimationFrame.flush();
expect(updateScroll).toHaveBeenCalledTimes(1);
-
- wrapper.unmount();
});
it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -545,18 +853,11 @@ describe('DraggableDimensionPublisher', () => {
requestAnimationFrame.flush();
expect(updateScroll).toHaveBeenCalledTimes(1);
-
- wrapper.unmount();
});
it('should stop watching for scroll events when the component is unmounted', () => {
- const publish = jest.fn();
- const updateScroll = jest.fn();
- const wrapper = mount(
-
,
+ wrapper = mount(
+
,
);
wrapper.setProps({
shouldPublish: true,
@@ -572,7 +873,7 @@ describe('DraggableDimensionPublisher', () => {
wrapper.unmount();
- // second event
+ // second event - will not fire any updates
container.scrollTop = 1001;
container.scrollLeft = 501;
container.dispatchEvent(new Event('scroll'));
@@ -580,4 +881,126 @@ describe('DraggableDimensionPublisher', () => {
expect(updateScroll).toHaveBeenCalledTimes(1);
});
});
+
+ describe('is enabled flag publishing', () => {
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
+ top: droppable.page.withoutMargin.top,
+ bottom: droppable.page.withoutMargin.bottom,
+ left: droppable.page.withoutMargin.left,
+ right: droppable.page.withoutMargin.right,
+ height: droppable.page.withoutMargin.height,
+ width: droppable.page.withoutMargin.width,
+ }));
+ jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
+ });
+
+ it('should publish whether the droppable is enabled when requested to publish', () => {
+ describe('enabled on mount', () => {
+ it('should publish that the dimension is enabled', () => {
+ wrapper = mount(
+
,
+ );
+ wrapper.setProps({
+ shouldPublish: true,
+ });
+
+ expect(publish).toBeCalledWith(droppable);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('disabled on mount', () => {
+ it('should publish that the dimension is disabled', () => {
+ const expected = {
+ ...droppable,
+ isEnabled: false,
+ };
+
+ wrapper = mount(
+
,
+ );
+ wrapper.setProps({
+ shouldPublish: true,
+ });
+
+ expect(publish).toBeCalledWith(expected);
+ expect(publish).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('should publish changes to the enabled state of the droppable during a drag', () => {
+ wrapper = mount(
+
,
+ );
+
+ // initial publish
+ wrapper.setProps({
+ shouldPublish: true,
+ });
+ expect(publish.mock.calls[0][0].isEnabled).toBe(true);
+
+ // disable
+ wrapper.setProps({
+ isDropDisabled: true,
+ });
+ expect(updateIsEnabled).toHaveBeenCalledTimes(1);
+ expect(updateIsEnabled.mock.calls[0]).toEqual([droppable.id, false]);
+
+ // enable
+ wrapper.setProps({
+ isDropDisabled: false,
+ });
+ expect(updateIsEnabled).toHaveBeenCalledTimes(2);
+ expect(updateIsEnabled.mock.calls[1]).toEqual([droppable.id, true]);
+ });
+
+ it('should not publish changes to the enabled state of the droppable when a drag is not occuring', () => {
+ wrapper = mount(
+
,
+ );
+ const change = () => {
+ // disabling
+ wrapper.setProps({
+ isDropDisabled: true,
+ });
+ // enabling
+ wrapper.setProps({
+ isDropDisabled: false,
+ });
+ };
+ // not publishing yet
+ change();
+ expect(updateIsEnabled).not.toHaveBeenCalled();
+
+ // now publishing
+ wrapper.setProps({
+ shouldPublish: true,
+ });
+
+ // this change will now trigger an update x 2
+ change();
+ expect(updateIsEnabled).toHaveBeenCalledTimes(2);
+ // disabling
+ expect(updateIsEnabled.mock.calls[0]).toEqual([droppable.id, false]);
+ // enabling
+ expect(updateIsEnabled.mock.calls[1]).toEqual([droppable.id, true]);
+
+ // no longer publishing
+ wrapper.setProps({
+ shouldPublish: false,
+ });
+
+ // should not do anything
+ change();
+ expect(updateIsEnabled).toHaveBeenCalledTimes(2);
+ });
+ });
});
diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js
index e2ba96fc7b..4cef5fc464 100644
--- a/test/unit/view/unconnected-droppable.spec.js
+++ b/test/unit/view/unconnected-droppable.spec.js
@@ -4,6 +4,7 @@ import { mount } from 'enzyme';
// eslint-disable-next-line no-duplicate-imports
import type { ReactWrapper } from 'enzyme';
import Droppable from '../../../src/view/droppable/droppable';
+import Placeholder from '../../../src/view/placeholder/';
import { withStore } from '../../utils/get-context-options';
import type { DroppableId } from '../../../src/types';
import type { MapProps, OwnProps, Provided, StateSnapshot } from '../../../src/view/droppable/droppable-types';
@@ -27,9 +28,19 @@ const getStubber = (mock: Function) =>
const defaultDroppableId: DroppableId = 'droppable-1';
const notDraggingOverMapProps: MapProps = {
isDraggingOver: false,
+ placeholder: null,
};
-const isDraggingOverMapProps: MapProps = {
+const isDraggingOverHomeMapProps: MapProps = {
isDraggingOver: true,
+ placeholder: null,
+};
+
+const isDraggingOverForeignMapProps: MapProps = {
+ isDraggingOver: true,
+ placeholder: {
+ width: 100,
+ height: 50,
+ },
};
// $ExpectError - not providing children
@@ -60,23 +71,55 @@ const mountDroppable = ({
, withStore());
describe('Droppable - unconnected', () => {
- it('should provide the props to its children', () => {
- const props: MapProps[] = [
- notDraggingOverMapProps, isDraggingOverMapProps,
- ];
-
- props.forEach((mapProps: MapProps) => {
+ describe('dragging over home droppable', () => {
+ it('should provide the props to its children', () => {
const myMock = jest.fn();
+ mountDroppable({
+ mapProps: isDraggingOverHomeMapProps,
+ WrappedComponent: getStubber(myMock),
+ });
+ const provided: Provided = myMock.mock.calls[0][0].provided;
+ const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
+ expect(provided.innerRef).toBeInstanceOf(Function);
+ expect(snapshot.isDraggingOver).toBe(true);
+ expect(provided.placeholder).toBe(null);
+ });
+ });
+
+ describe('dragging over foreign droppable', () => {
+ it('should provide the props to its children', () => {
+ const myMock = jest.fn();
mountDroppable({
- mapProps,
+ mapProps: isDraggingOverForeignMapProps,
WrappedComponent: getStubber(myMock),
});
const provided: Provided = myMock.mock.calls[0][0].provided;
const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
expect(provided.innerRef).toBeInstanceOf(Function);
- expect(snapshot.isDraggingOver).toBe(mapProps.isDraggingOver);
+ expect(snapshot.isDraggingOver).toBe(true);
+ // $ExpectError - type property of placeholder
+ expect(provided.placeholder.type).toBe(Placeholder);
+ // $ExpectError - props property of placeholder
+ expect(provided.placeholder.props).toEqual(isDraggingOverForeignMapProps.placeholder);
+ });
+
+ describe('not dragging over droppable', () => {
+ it('should provide the props to its children', () => {
+ const myMock = jest.fn();
+ mountDroppable({
+ mapProps: notDraggingOverMapProps,
+ WrappedComponent: getStubber(myMock),
+ });
+
+ const provided: Provided = myMock.mock.calls[0][0].provided;
+ const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
+ expect(provided.innerRef).toBeInstanceOf(Function);
+ expect(snapshot.isDraggingOver).toBe(false);
+ expect(provided.placeholder).toBe(null);
+ });
});
});
});
+
diff --git a/test/utils/get-client-rect.js b/test/utils/get-client-rect.js
deleted file mode 100644
index ccc4f3254b..0000000000
--- a/test/utils/get-client-rect.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { ClientRect } from '../../src/state/dimension';
-
-type GetClientRect = {|
- top: number,
- right: number,
- bottom: number,
- left: number,
-|}
-
-export default ({ top, right, bottom, left }: GetClientRect): ClientRect => ({
- top,
- right,
- bottom,
- left,
- width: (right - left),
- height: (bottom - top),
-});
diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js
new file mode 100644
index 0000000000..d0c809b856
--- /dev/null
+++ b/test/utils/get-droppable-with-draggables.js
@@ -0,0 +1,65 @@
+// @flow
+import { getDraggableDimension, getDroppableDimension } from '../../src/state/dimension';
+import type {
+ DroppableId,
+ DraggableDimension,
+ DraggableDimensionMap,
+ DroppableDimension,
+ Direction,
+ Spacing,
+} from '../../src/types';
+import getClientRect from '../../src/state/get-client-rect';
+
+type Args = {|
+ direction?: Direction,
+ droppableId?: DroppableId,
+ droppableRect: Spacing,
+ draggableRects: Spacing[],
+|};
+
+export type Result = {
+ droppableId: string,
+ droppable: DroppableDimension,
+ draggables: DraggableDimensionMap,
+ draggableIds: string[],
+ draggableDimensions: DraggableDimension[],
+};
+
+let idCount = 0;
+
+export default ({
+ direction = 'vertical',
+ droppableId = `droppable-generated-id-${idCount++}`,
+ droppableRect,
+ draggableRects,
+}: Args): Result => {
+ const droppable: DroppableDimension = getDroppableDimension({
+ id: droppableId,
+ direction,
+ clientRect: getClientRect(droppableRect),
+ });
+
+ const draggableDimensions: DraggableDimension[] = draggableRects.map(
+ (draggableRect, index) => getDraggableDimension({
+ id: `${droppableId}::drag-${index}`,
+ droppableId,
+ clientRect: getClientRect(draggableRect),
+ })
+ );
+
+ const draggables: DraggableDimensionMap = draggableDimensions.reduce(
+ (currentDraggables, draggable) => ({
+ ...currentDraggables,
+ [draggable.id]: draggable,
+ }), {});
+
+ const draggableIds = Object.keys(draggables);
+
+ return {
+ droppableId,
+ droppable,
+ draggables,
+ draggableIds,
+ draggableDimensions,
+ };
+};
diff --git a/test/utils/get-fragment.js b/test/utils/get-fragment.js
new file mode 100644
index 0000000000..eaa7054035
--- /dev/null
+++ b/test/utils/get-fragment.js
@@ -0,0 +1,16 @@
+// @flow
+import type { DimensionFragment, ClientRect } from '../../src/types';
+
+export default (clientRect: ClientRect): DimensionFragment => {
+ const { top, left, bottom, right, width, height } = clientRect;
+ const center = {
+ x: (right + left) / 2,
+ y: (top + bottom) / 2,
+ };
+
+ const fragment: DimensionFragment = {
+ top, left, bottom, right, width, height, center,
+ };
+
+ return fragment;
+};