diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js
index 24966a9efd..c6c19586f4 100644
--- a/test/unit/state/action-creators.spec.js
+++ b/test/unit/state/action-creators.spec.js
@@ -7,18 +7,19 @@ import {
prepare,
completeLift,
requestDimensions,
- publishDraggableDimensions,
- publishDroppableDimensions,
+ publishDraggableDimension,
+ publishDroppableDimension,
} from '../../../src/state/action-creators';
import createStore from '../../../src/state/create-store';
import { getPreset } from '../../utils/dimension';
-import * as state from '../../utils/simple-state-preset';
+import getStatePreset from '../../utils/get-simple-state-preset';
import type {
State,
Position,
DraggableId,
Store,
InitialDragPositions,
+ LiftRequest,
} from '../../../src/types';
const preset = getPreset();
@@ -32,19 +33,21 @@ type LiftFnArgs = {|
id: DraggableId,
client: InitialDragPositions,
windowScroll: Position,
- isScrollAllowed: boolean,
+ autoScrollMode: 'FLUID' | 'JUMP',
|}
const liftDefaults: LiftFnArgs = {
id: preset.inHome1.descriptor.id,
windowScroll: origin,
client: noWhere,
- isScrollAllowed: true,
+ autoScrollMode: 'FLUID',
};
+const state = getStatePreset();
+
const liftWithDefaults = (args?: LiftFnArgs = liftDefaults) => {
- const { id, client, windowScroll, isScrollAllowed } = args;
- return lift(id, client, windowScroll, isScrollAllowed);
+ const { id, client, windowScroll, autoScrollMode } = args;
+ return lift(id, client, windowScroll, autoScrollMode);
};
describe('action creators', () => {
@@ -74,12 +77,18 @@ describe('action creators', () => {
// Phase 2: request dimensions after flushing animations
jest.runOnlyPendingTimers();
- expect(store.dispatch).toHaveBeenCalledWith(requestDimensions(preset.inHome1.descriptor.id));
+ const request: LiftRequest = {
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: {
+ shouldPublishImmediately: false,
+ },
+ };
+ expect(store.dispatch).toHaveBeenCalledWith(requestDimensions(request));
expect(store.dispatch).toHaveBeenCalledTimes(2);
// publishing some fake dimensions
- store.dispatch(publishDroppableDimensions([preset.home]));
- store.dispatch(publishDraggableDimensions([preset.inHome1]));
+ store.dispatch(publishDroppableDimension(preset.home));
+ store.dispatch(publishDraggableDimension(preset.inHome1));
// now called four times
expect(store.dispatch).toHaveBeenCalledTimes(4);
@@ -90,7 +99,7 @@ describe('action creators', () => {
liftDefaults.id,
liftDefaults.client,
liftDefaults.windowScroll,
- liftDefaults.isScrollAllowed
+ liftDefaults.autoScrollMode,
));
expect(store.dispatch).toHaveBeenCalledTimes(5);
});
@@ -152,7 +161,7 @@ describe('action creators', () => {
liftDefaults.id,
liftDefaults.client,
liftDefaults.windowScroll,
- liftDefaults.isScrollAllowed
+ liftDefaults.autoScrollMode,
)
);
@@ -176,7 +185,12 @@ describe('action creators', () => {
jest.runOnlyPendingTimers();
expect(store.dispatch).toHaveBeenCalledWith(
- requestDimensions(preset.inHome1.descriptor.id)
+ requestDimensions({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: {
+ shouldPublishImmediately: false,
+ },
+ })
);
expect(store.dispatch).toHaveBeenCalledTimes(2);
@@ -193,7 +207,7 @@ describe('action creators', () => {
liftDefaults.id,
liftDefaults.client,
liftDefaults.windowScroll,
- liftDefaults.isScrollAllowed
+ liftDefaults.autoScrollMode,
));
// being super careful
diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js
new file mode 100644
index 0000000000..291a041420
--- /dev/null
+++ b/test/unit/state/auto-scroll/can-scroll.spec.js
@@ -0,0 +1,591 @@
+// @flow
+import type {
+ Area,
+ Position,
+ DroppableDimension,
+} from '../../../../src/types';
+import {
+ canPartiallyScroll,
+ getOverlap,
+ getWindowOverlap,
+ getDroppableOverlap,
+ canScrollDroppable,
+ canScrollWindow,
+} from '../../../../src/state/auto-scroller/can-scroll';
+import { add, subtract } from '../../../../src/state/position';
+import getArea from '../../../../src/state/get-area';
+import { getPreset } from '../../../utils/dimension';
+import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
+import setViewport, { resetViewport } from '../../../utils/set-viewport';
+import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll';
+import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size';
+import getMaxScroll from '../../../../src/state/get-max-scroll';
+
+const origin: Position = { x: 0, y: 0 };
+const preset = getPreset();
+
+const scrollableScrollSize = {
+ scrollWidth: 200,
+ scrollHeight: 200,
+};
+
+const scrollable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'drop-1',
+ type: 'TYPE',
+ },
+ client: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ // bigger than the frame
+ bottom: 200,
+ }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ scrollWidth: scrollableScrollSize.scrollWidth,
+ scrollHeight: scrollableScrollSize.scrollHeight,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+});
+
+describe('can scroll', () => {
+ afterEach(() => {
+ resetWindowScroll();
+ resetWindowScrollSize();
+ resetViewport();
+ });
+
+ describe('can partially scroll', () => {
+ it('should return true if not scrolling anywhere', () => {
+ const result: boolean = canPartiallyScroll({
+ max: { x: 100, y: 100 },
+ current: { x: 0, y: 0 },
+ // not
+ change: origin,
+ });
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true if scrolling to a boundary', () => {
+ const current: Position = origin;
+ const max: Position = { x: 100, y: 200 };
+
+ const corners: Position[] = [
+ // top left
+ { x: 0, y: 0 },
+ // top right
+ { x: max.x, y: 0 },
+ // bottom right
+ { x: max.x, y: max.y },
+ // bottom left
+ { x: 0, y: max.y },
+ ];
+
+ corners.forEach((corner: Position) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current,
+ change: corner,
+ });
+
+ expect(result).toBe(true);
+ });
+ });
+
+ it('should return true if moving in any direction within the allowable scroll region', () => {
+ const max: Position = { x: 100, y: 100 };
+ const current: Position = { x: 50, y: 50 };
+
+ // all of these movements are totally possible
+ const changes: Position[] = [
+ // top left
+ { x: -10, y: 10 },
+ // top right
+ { x: 10, y: 10 },
+ // bottom right
+ { x: 10, y: -10 },
+ // bottom left
+ { x: -10, y: -10 },
+ ];
+
+ changes.forEach((point: Position) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current,
+ change: point,
+ });
+
+ expect(result).toBe(true);
+ });
+ });
+
+ it('should return true if able to partially move in both directions', () => {
+ const max: Position = { x: 100, y: 100 };
+ const current: Position = { x: 50, y: 50 };
+
+ // all of these movements are partially possible
+ const changes: Position[] = [
+ // top left
+ { x: -200, y: 200 },
+ // top right
+ { x: 200, y: 200 },
+ // bottom right
+ { x: 200, y: -200 },
+ // bottom left
+ { x: -200, y: -200 },
+ ];
+
+ changes.forEach((point: Position) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current,
+ change: point,
+ });
+
+ expect(result).toBe(true);
+ });
+ });
+
+ type Item = {|
+ current: Position,
+ change: Position,
+ |}
+
+ it('should return true if can only partially move in one direction', () => {
+ const max: Position = { x: 100, y: 200 };
+
+ const changes: Item[] = [
+ // Can move back in the y direction, but not back in the x direction
+ {
+ current: { x: 0, y: 1 },
+ change: { x: -1, y: -1 },
+ },
+ // Can move back in the x direction, but not back in the y direction
+ {
+ current: { x: 1, y: 0 },
+ change: { x: -1, y: -1 },
+ },
+ // Can move forward in the y direction, but not forward in the x direction
+ {
+ current: subtract(max, { x: 0, y: 1 }),
+ change: { x: 1, y: 1 },
+ },
+ // Can move forward in the x direction, but not forward in the y direction
+ {
+ current: subtract(max, { x: 1, y: 0 }),
+ change: { x: 1, y: 1 },
+ },
+ ];
+
+ changes.forEach((item: Item) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current: item.current,
+ change: item.change,
+ });
+
+ expect(result).toBe(true);
+ });
+ });
+
+ it('should return false if on the min point and move backward in any direction', () => {
+ const current: Position = origin;
+ const max: Position = { x: 100, y: 200 };
+ const tooFarBack: Position[] = [
+ { x: 0, y: -1 },
+ { x: -1, y: 0 },
+ ];
+
+ tooFarBack.forEach((point: Position) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current,
+ change: point,
+ });
+
+ expect(result).toBe(false);
+ });
+ });
+
+ it('should return false if on the max point and move forward in any direction', () => {
+ const max: Position = { x: 100, y: 200 };
+ const current: Position = max;
+ const tooFarForward: Position[] = [
+ add(max, { x: 0, y: 1 }),
+ add(max, { x: 1, y: 0 }),
+ ];
+
+ tooFarForward.forEach((point: Position) => {
+ const result: boolean = canPartiallyScroll({
+ max,
+ current,
+ change: point,
+ });
+
+ expect(result).toBe(false);
+ });
+ });
+ });
+
+ describe('get overlap', () => {
+ describe('returning the remainder', () => {
+ const max: Position = { x: 100, y: 100 };
+ const current: Position = { x: 50, y: 50 };
+
+ type Item = {|
+ change: Position,
+ expected: Position,
+ |}
+
+ it('should return overlap on a single axis', () => {
+ const items: Item[] = [
+ // too far back: top
+ {
+ change: { x: 0, y: -70 },
+ expected: { x: 0, y: -20 },
+ },
+ // too far back: left
+ {
+ change: { x: -70, y: 0 },
+ expected: { x: -20, y: 0 },
+ },
+ // too far forward: right
+ {
+ change: { x: 70, y: 0 },
+ expected: { x: 20, y: 0 },
+ },
+ // too far forward: bottom
+ {
+ change: { x: 0, y: 70 },
+ expected: { x: 0, y: 20 },
+ },
+ ];
+
+ items.forEach((item: Item) => {
+ const result: ?Position = getOverlap({
+ current,
+ max,
+ change: item.change,
+ });
+
+ expect(result).toEqual(item.expected);
+ });
+ });
+
+ it('should return overlap on two axis in the same direction', () => {
+ const items: Item[] = [
+ // too far back: top
+ {
+ change: { x: -80, y: -70 },
+ expected: { x: -30, y: -20 },
+ },
+ // too far back: left
+ {
+ change: { x: -70, y: -80 },
+ expected: { x: -20, y: -30 },
+ },
+ // too far forward: right
+ {
+ change: { x: 70, y: 0 },
+ expected: { x: 20, y: 0 },
+ },
+ // too far forward: bottom
+ {
+ change: { x: 80, y: 70 },
+ expected: { x: 30, y: 20 },
+ },
+ ];
+
+ items.forEach((item: Item) => {
+ const result: ?Position = getOverlap({
+ current,
+ max,
+ change: item.change,
+ });
+
+ expect(result).toEqual(item.expected);
+ });
+ });
+
+ it('should return overlap on two axis in different directions', () => {
+ const items: Item[] = [
+ // too far back: vertical
+ // too far forward: horizontal
+ {
+ change: { x: 80, y: -70 },
+ expected: { x: 30, y: -20 },
+ },
+ // too far back: horizontal
+ // too far forward: vertical
+ {
+ change: { x: -70, y: 80 },
+ expected: { x: -20, y: 30 },
+ },
+ ];
+
+ items.forEach((item: Item) => {
+ const result: ?Position = getOverlap({
+ current,
+ max,
+ change: item.change,
+ });
+
+ expect(result).toEqual(item.expected);
+ });
+ });
+
+ it('should trim values that can be scrolled', () => {
+ const items: Item[] = [
+ // too far back: top
+ {
+ // x can be scrolled entirely
+ // y can be partially scrolled
+ change: { x: -20, y: -70 },
+ expected: { x: 0, y: -20 },
+ },
+ // too far back: left
+ {
+ // x can be partially scrolled
+ // y can be scrolled entirely
+ change: { x: -70, y: -40 },
+ expected: { x: -20, y: 0 },
+ },
+ // too far forward: right
+ {
+ // x can be partially scrolled
+ // y can be scrolled entirely
+ change: { x: 70, y: 40 },
+ expected: { x: 20, y: 0 },
+ },
+ // too far forward: bottom
+ {
+ // x can be scrolled entirely
+ // y can be partially scrolled
+ change: { x: 20, y: 70 },
+ expected: { x: 0, y: 20 },
+ },
+
+ ];
+
+ items.forEach((item: Item) => {
+ const result: ?Position = getOverlap({
+ current,
+ max,
+ change: item.change,
+ });
+
+ expect(result).toEqual(item.expected);
+ });
+ });
+ });
+ });
+
+ describe('can scroll droppable', () => {
+ it('should return false if the droppable is not scrollable', () => {
+ const result: boolean = canScrollDroppable(preset.home, { x: 1, y: 1 });
+
+ expect(result).toBe(false);
+ });
+
+ it('should return true if the droppable is able to be scrolled', () => {
+ const result: boolean = canScrollDroppable(scrollable, { x: 0, y: 20 });
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if the droppable is not able to be scrolled', () => {
+ const result: boolean = canScrollDroppable(scrollable, { x: -1, y: 0 });
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('can scroll window', () => {
+ it('should return true if the window is able to be scrolled', () => {
+ setViewport(getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }));
+ setWindowScrollSize({
+ scrollHeight: 200,
+ scrollWidth: 100,
+ });
+ setWindowScroll(origin);
+
+ const result: boolean = canScrollWindow({ x: 0, y: 50 });
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if the window is not able to be scrolled', () => {
+ setViewport(getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }));
+ setWindowScrollSize({
+ scrollHeight: 200,
+ scrollWidth: 100,
+ });
+ // already at the max scroll
+ setWindowScroll({
+ x: 0,
+ y: 200,
+ });
+
+ const result: boolean = canScrollWindow({ x: 0, y: 1 });
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe('get droppable overlap', () => {
+ it('should return null if there is no scroll container', () => {
+ const result: ?Position = getDroppableOverlap(preset.home, { x: 1, y: 1 });
+
+ expect(result).toBe(null);
+ });
+
+ it('should return null if the droppable cannot be scrolled', () => {
+ // end of the scrollable area
+ const scroll: Position = {
+ x: 0,
+ y: 200,
+ };
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, scroll);
+ const result: ?Position = getDroppableOverlap(scrolled, { x: 0, y: 1 });
+
+ expect(result).toBe(null);
+ });
+
+ // tested in get remainder
+ it('should return the overlap', () => {
+ // how far the droppable has already
+ const scroll: Position = {
+ x: 10, y: 20,
+ };
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, scroll);
+ // $ExpectError - not checking for null
+ const max: Position = scrolled.viewport.closestScrollable.scroll.max;
+ const totalSpace: Position = {
+ x: scrollableScrollSize.scrollWidth - max.x,
+ y: scrollableScrollSize.scrollHeight - max.y,
+ };
+ const remainingSpace = subtract(totalSpace, scroll);
+ const change: Position = { x: 300, y: 300 };
+ const expectedOverlap: Position = subtract(change, remainingSpace);
+
+ const result: ?Position = getDroppableOverlap(scrolled, change);
+
+ expect(result).toEqual(expectedOverlap);
+ });
+
+ it('should return null if there is no overlap', () => {
+ const change: Position = { x: 0, y: 1 };
+
+ const result: ?Position = getDroppableOverlap(scrollable, change);
+
+ expect(result).toEqual(null);
+
+ // verifying correctness of test
+
+ expect(canScrollDroppable(scrollable, change)).toBe(true);
+ });
+ });
+
+ describe('get window overlap', () => {
+ it('should return null if the window cannot be scrolled', () => {
+ setViewport(getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }));
+ setWindowScrollSize({
+ scrollHeight: 200,
+ scrollWidth: 100,
+ });
+ // already at the max scroll
+ setWindowScroll({
+ x: 0,
+ y: 200,
+ });
+
+ const result: ?Position = getWindowOverlap({ x: 0, y: 1 });
+
+ expect(result).toBe(null);
+ });
+
+ // tested in get remainder
+ it('should return the overlap', () => {
+ const viewport: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ });
+ setViewport(viewport);
+ const windowScrollSize = {
+ scrollHeight: 200,
+ scrollWidth: 200,
+ };
+ setWindowScrollSize(windowScrollSize);
+ const windowScroll: Position = {
+ x: 50,
+ y: 50,
+ };
+ setWindowScroll(windowScroll);
+
+ // little validation
+ const maxScroll: Position = getMaxScroll({
+ scrollHeight: windowScrollSize.scrollHeight,
+ scrollWidth: windowScrollSize.scrollWidth,
+ height: viewport.height,
+ width: viewport.width,
+ });
+ expect(maxScroll).toEqual({ x: 100, y: 100 });
+
+ const availableScrollSpace: Position = {
+ x: 50,
+ y: 50,
+ };
+ // cannot be absorbed in the current scroll plane
+ const bigChange: Position = { x: 300, y: 300 };
+ const expectedOverlap: Position = subtract(bigChange, availableScrollSpace);
+
+ const result: ?Position = getWindowOverlap(bigChange);
+
+ expect(result).toEqual(expectedOverlap);
+ });
+
+ it('should return null if there is no overlap', () => {
+ setViewport(getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }));
+ const scrollSize = {
+ scrollHeight: 100,
+ scrollWidth: 100,
+ };
+ setWindowScrollSize(scrollSize);
+ setWindowScroll(origin);
+
+ const result: ?Position = getWindowOverlap({ x: 10, y: 10 });
+
+ expect(result).toBe(null);
+ });
+ });
+});
diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js
new file mode 100644
index 0000000000..116487135f
--- /dev/null
+++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js
@@ -0,0 +1,1486 @@
+// @flow
+import type {
+ Area,
+ Axis,
+ Position,
+ State,
+ DraggableDimension,
+ DroppableDimension,
+ DragImpact,
+} from '../../../../src/types';
+import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types';
+import type { PixelThresholds } from '../../../../src/state/auto-scroller/fluid-scroller';
+import { getPixelThresholds, config } from '../../../../src/state/auto-scroller/fluid-scroller';
+import { add, patch, subtract } from '../../../../src/state/position';
+import getArea from '../../../../src/state/get-area';
+import setViewport, { resetViewport } from '../../../utils/set-viewport';
+import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size';
+import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll';
+import noImpact, { noMovement } from '../../../../src/state/no-impact';
+import { vertical, horizontal } from '../../../../src/state/axis';
+import createAutoScroller from '../../../../src/state/auto-scroller/';
+import getStatePreset from '../../../utils/get-simple-state-preset';
+import {
+ getInitialImpact,
+ getClosestScrollable,
+ getPreset,
+ withImpact,
+ addDraggable,
+ addDroppable,
+} from '../../../utils/dimension';
+import { expandByPosition } from '../../../../src/state/spacing';
+import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
+
+const windowScrollSize = {
+ scrollHeight: 2000,
+ scrollWidth: 1600,
+};
+const viewport: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 800,
+ bottom: 1000,
+});
+
+describe('fluid auto scrolling', () => {
+ let autoScroller: AutoScroller;
+ let mocks;
+
+ beforeEach(() => {
+ mocks = {
+ scrollWindow: jest.fn(),
+ scrollDroppable: jest.fn(),
+ move: jest.fn(),
+ };
+ autoScroller = createAutoScroller(mocks);
+ setViewport(viewport);
+ setWindowScrollSize(windowScrollSize);
+ });
+
+ afterEach(() => {
+ resetWindowScroll();
+ resetWindowScrollSize();
+ resetViewport();
+ requestAnimationFrame.reset();
+ });
+
+ [vertical, horizontal].forEach((axis: Axis) => {
+ describe(`on the ${axis.direction} axis`, () => {
+ const preset = getPreset(axis);
+ const state = getStatePreset(axis);
+ const scrollableScrollSize = {
+ scrollWidth: 800,
+ scrollHeight: 800,
+ };
+ const frame: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 600,
+ bottom: 600,
+ });
+
+ const scrollable: DroppableDimension = getDroppableDimension({
+ // stealing the home descriptor
+ descriptor: preset.home.descriptor,
+ direction: axis.direction,
+ client: getArea({
+ top: 0,
+ left: 0,
+ // bigger than the frame
+ right: scrollableScrollSize.scrollWidth,
+ bottom: scrollableScrollSize.scrollHeight,
+ }),
+ closest: {
+ frameClient: frame,
+ scrollWidth: scrollableScrollSize.scrollWidth,
+ scrollHeight: scrollableScrollSize.scrollHeight,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ const dragTo = (
+ selection: Position,
+ // seeding that we are over the home droppable
+ impact?: DragImpact = getInitialImpact(preset.inHome1, axis),
+ ): State => withImpact(
+ state.dragging(preset.inHome1.descriptor.id, selection),
+ impact,
+ );
+
+ describe('window scrolling', () => {
+ const thresholds: PixelThresholds = getPixelThresholds(viewport, axis);
+ const crossAxisThresholds: PixelThresholds = getPixelThresholds(
+ viewport,
+ axis === vertical ? horizontal : vertical,
+ );
+
+ describe('moving forward to end of window', () => {
+ const onStartBoundary: Position = patch(
+ axis.line,
+ // to the boundary is not enough to start
+ (viewport[axis.size] - thresholds.startFrom),
+ viewport.center[axis.crossAxisLine],
+ );
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (viewport[axis.size] - thresholds.maxSpeedAt),
+ viewport.center[axis.crossAxisLine],
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(state.idle, dragTo(onStartBoundary));
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = add(onStartBoundary, patch(axis.line, 1));
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalled();
+ // moving forwards
+ const request: Position = mocks.scrollWindow.mock.calls[0][0];
+ expect(request[axis.line]).toBeGreaterThan(0);
+ });
+
+ it('should throttle multiple scrolls into a single animation frame', () => {
+ const target1: Position = add(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = add(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(state.idle, dragTo(target1));
+ autoScroller.onStateChange(dragTo(target1), dragTo(target2));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+
+ // verification
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+
+ // not testing value called as we are not exposing getRequired scroll
+ });
+
+ it('should get faster the closer to the max speed point', () => {
+ const target1: Position = add(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = add(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(state.idle, dragTo(target1));
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+ const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any);
+
+ autoScroller.onStateChange(dragTo(target1), dragTo(target2));
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(2);
+ const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any);
+
+ expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]);
+
+ // validation
+ expect(scroll1[axis.crossAxisLine]).toBe(0);
+ expect(scroll2[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should have the top speed at the max speed point', () => {
+ const expected: Position = patch(axis.line, config.maxScrollSpeed);
+
+ autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary));
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(expected);
+ });
+
+ it('should have the top speed when moving beyond the max speed point', () => {
+ const target: Position = add(onMaxBoundary, patch(axis.line, 1));
+ const expected: Position = patch(axis.line, config.maxScrollSpeed);
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(expected);
+ });
+
+ it('should not scroll if the item is too big', () => {
+ const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 }));
+ const tooBig: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+ const selection: Position = onMaxBoundary;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBig.descriptor,
+ },
+ },
+ };
+
+ return addDraggable(updated, tooBig);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should not scroll if the window cannot scroll', () => {
+ setWindowScrollSize({
+ scrollHeight: viewport.height,
+ scrollWidth: viewport.width,
+ });
+ const target: Position = onMaxBoundary;
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('moving backwards towards the start of window', () => {
+ const windowScroll: Position = patch(axis.line, 10);
+
+ beforeEach(() => {
+ setWindowScroll(windowScroll);
+ });
+
+ const onStartBoundary: Position = patch(
+ axis.line,
+ // at the boundary is not enough to start
+ windowScroll[axis.line] + thresholds.startFrom,
+ viewport.center[axis.crossAxisLine],
+ );
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (windowScroll[axis.line] + thresholds.maxSpeedAt),
+ viewport.center[axis.crossAxisLine],
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(state.idle, dragTo(onStartBoundary));
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = subtract(onStartBoundary, patch(axis.line, 1));
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalled();
+ // moving backwards
+ const request: Position = mocks.scrollWindow.mock.calls[0][0];
+ expect(request[axis.line]).toBeLessThan(0);
+ });
+
+ it('should throttle multiple scrolls into a single animation frame', () => {
+ const target1: Position = subtract(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = subtract(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(state.idle, dragTo(target1));
+ autoScroller.onStateChange(dragTo(target1), dragTo(target2));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+
+ // verification
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+
+ // not testing value called as we are not exposing getRequired scroll
+ });
+
+ it('should get faster the closer to the max speed point', () => {
+ const target1: Position = subtract(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = subtract(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(state.idle, dragTo(target1));
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(1);
+ const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any);
+
+ autoScroller.onStateChange(dragTo(target1), dragTo(target2));
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledTimes(2);
+ const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any);
+
+ // moving backwards so a smaller value is bigger
+ expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]);
+ // or put another way:
+ expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line]));
+
+ // validation
+ expect(scroll1[axis.crossAxisLine]).toBe(0);
+ expect(scroll2[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should have the top speed at the max speed point', () => {
+ const target: Position = onMaxBoundary;
+ const expected: Position = patch(axis.line, -config.maxScrollSpeed);
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(expected);
+ });
+
+ it('should have the top speed when moving beyond the max speed point', () => {
+ const target: Position = subtract(onMaxBoundary, patch(axis.line, 1));
+ const expected: Position = patch(axis.line, -config.maxScrollSpeed);
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(expected);
+ });
+
+ it('should not scroll if the item is too big', () => {
+ const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 }));
+ const tooBig: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+ const selection: Position = onMaxBoundary;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBig.descriptor,
+ },
+ },
+ };
+
+ return addDraggable(updated, tooBig);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should not scroll if the window cannot scroll', () => {
+ setWindowScrollSize({
+ scrollHeight: viewport.height,
+ scrollWidth: viewport.width,
+ });
+ const target: Position = onMaxBoundary;
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+ });
+
+ // just some light tests to ensure that cross axis moving also works
+ describe('moving forward on the cross axis', () => {
+ const onStartBoundary: Position = patch(
+ axis.line,
+ viewport.center[axis.line],
+ // to the boundary is not enough to start
+ (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom),
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(state.idle, dragTo(onStartBoundary));
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1));
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalled();
+ // moving forwards
+ const request: Position = mocks.scrollWindow.mock.calls[0][0];
+ expect(request[axis.crossAxisLine]).toBeGreaterThan(0);
+ });
+ });
+
+ // just some light tests to ensure that cross axis moving also works
+ describe('moving backward on the cross axis', () => {
+ const windowScroll: Position = patch(axis.crossAxisLine, 10);
+ beforeEach(() => {
+ setWindowScroll(windowScroll);
+ });
+
+ const onStartBoundary: Position = patch(
+ axis.line,
+ viewport.center[axis.line],
+ // to the boundary is not enough to start
+ windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom)
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(state.idle, dragTo(onStartBoundary));
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1));
+
+ autoScroller.onStateChange(state.idle, dragTo(target));
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalled();
+ // moving backwards
+ const request: Position = mocks.scrollWindow.mock.calls[0][0];
+ expect(request[axis.crossAxisLine]).toBeLessThan(0);
+ });
+ });
+
+ describe('big draggable', () => {
+ const onMaxBoundaryOfBoth: Position = patch(
+ axis.line,
+ (viewport[axis.size] - thresholds.maxSpeedAt),
+ (viewport[axis.crossAxisSize] - crossAxisThresholds.maxSpeedAt),
+ );
+
+ describe('bigger on the main axis', () => {
+ it('should not allow scrolling on the main axis, but allow scrolling on the cross axis', () => {
+ const expanded: Area = getArea(expandByPosition(viewport, patch(axis.line, 1)));
+ const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBigOnMainAxis.descriptor,
+ },
+ },
+ };
+
+ return addDraggable(updated, tooBigOnMainAxis);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(
+ // scroll ocurred on the cross axis, but not on the main axis
+ patch(axis.crossAxisLine, config.maxScrollSpeed)
+ );
+ });
+ });
+
+ describe('bigger on the cross axis', () => {
+ it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => {
+ const expanded: Area = getArea(
+ expandByPosition(viewport, patch(axis.crossAxisLine, 1))
+ );
+ const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBigOnCrossAxis.descriptor,
+ },
+ },
+ };
+
+ return addDraggable(updated, tooBigOnCrossAxis);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(
+ // scroll ocurred on the main axis, but not on the cross axis
+ patch(axis.line, config.maxScrollSpeed)
+ );
+ });
+ });
+
+ describe('bigger on both axis', () => {
+ it('should not allow scrolling on any axis', () => {
+ const expanded: Area = getArea(
+ expandByPosition(viewport, patch(axis.line, 1, 1))
+ );
+ const tooBig: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBig.descriptor,
+ },
+ },
+ };
+
+ return addDraggable(updated, tooBig);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('droppable scrolling', () => {
+ const thresholds: PixelThresholds = getPixelThresholds(frame, axis);
+ const crossAxisThresholds: PixelThresholds = getPixelThresholds(
+ frame,
+ axis === vertical ? horizontal : vertical
+ );
+ const maxScrollSpeed: Position = patch(axis.line, config.maxScrollSpeed);
+
+ beforeEach(() => {
+ // avoiding any window scrolling
+ setWindowScrollSize({
+ scrollHeight: viewport.height,
+ scrollWidth: viewport.width,
+ });
+ });
+
+ describe('moving forward to end of droppable', () => {
+ const onStartBoundary: Position = patch(
+ axis.line,
+ // to the boundary is not enough to start
+ (frame[axis.size] - thresholds.startFrom),
+ frame.center[axis.crossAxisLine],
+ );
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (frame[axis.size] - thresholds.maxSpeedAt),
+ frame.center[axis.crossAxisLine],
+ );
+ const onEndOfFrame: Position = patch(
+ axis.line,
+ frame[axis.size],
+ frame.center[axis.crossAxisLine],
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onStartBoundary), scrollable)
+ );
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = add(onStartBoundary, patch(axis.line, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrollable),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalled();
+ // moving forwards
+ const [id, scroll] = mocks.scrollDroppable.mock.calls[0];
+
+ expect(id).toBe(scrollable.descriptor.id);
+ expect(scroll[axis.line]).toBeGreaterThan(0);
+ expect(scroll[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should throttle multiple scrolls into a single animation frame', () => {
+ const target1: Position = add(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = add(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target1), scrollable),
+ );
+ autoScroller.onStateChange(
+ addDroppable(dragTo(target1), scrollable),
+ addDroppable(dragTo(target2), scrollable),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+
+ // verification
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+
+ // not testing value called as we are not exposing getRequired scroll
+ });
+
+ it('should get faster the closer to the max speed point', () => {
+ const target1: Position = add(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = add(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target1), scrollable),
+ );
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+ const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any);
+
+ autoScroller.onStateChange(
+ addDroppable(dragTo(target1), scrollable),
+ addDroppable(dragTo(target2), scrollable),
+ );
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2);
+ const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any);
+
+ expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]);
+
+ // validation
+ expect(scroll1[axis.crossAxisLine]).toBe(0);
+ expect(scroll2[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should have the top speed at the max speed point', () => {
+ const expected: Position = patch(axis.line, config.maxScrollSpeed);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onMaxBoundary), scrollable),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ expected
+ );
+ });
+
+ it('should have the top speed when moving beyond the max speed point', () => {
+ const target: Position = add(onMaxBoundary, patch(axis.line, 1));
+ const expected: Position = patch(axis.line, config.maxScrollSpeed);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrollable),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ expected
+ );
+ });
+
+ it('should allow scrolling to the end of the droppable', () => {
+ const target: Position = onEndOfFrame;
+ // scrolling to max scroll point
+ const maxChange: Position = getClosestScrollable(scrollable).scroll.max;
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ describe('big draggable', () => {
+ const onMaxBoundaryOfBoth: Position = patch(
+ axis.line,
+ (frame[axis.size] - thresholds.maxSpeedAt),
+ (frame[axis.crossAxisSize] - crossAxisThresholds.maxSpeedAt),
+ );
+
+ describe('bigger on the main axis', () => {
+ it('should not allow scrolling on the main axis, but allow scrolling on the cross axis', () => {
+ const expanded: Area = getArea(expandByPosition(frame, patch(axis.line, 1)));
+ const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBigOnMainAxis.descriptor,
+ },
+ },
+ };
+
+ return addDroppable(addDraggable(updated, tooBigOnMainAxis), scrollable);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ // scroll ocurred on the cross axis, but not on the main axis
+ patch(axis.crossAxisLine, config.maxScrollSpeed)
+ );
+ });
+ });
+
+ describe('bigger on the cross axis', () => {
+ it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => {
+ const expanded: Area = getArea(
+ expandByPosition(frame, patch(axis.crossAxisLine, 1))
+ );
+ const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBigOnCrossAxis.descriptor,
+ },
+ },
+ };
+
+ return addDroppable(addDraggable(updated, tooBigOnCrossAxis), scrollable);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ // scroll ocurred on the main axis, but not on the cross axis
+ patch(axis.line, config.maxScrollSpeed)
+ );
+ });
+ });
+
+ describe('bigger on both axis', () => {
+ it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => {
+ const expanded: Area = getArea(
+ expandByPosition(frame, patch(axis.line, 1, 1))
+ );
+ const tooBig: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+
+ const selection: Position = onMaxBoundaryOfBoth;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBig.descriptor,
+ },
+ },
+ };
+
+ return addDroppable(addDraggable(updated, tooBig), scrollable);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('over home list', () => {
+ it('should not scroll if the droppable if moving past the end of the frame', () => {
+ const target: Position = add(onEndOfFrame, patch(axis.line, 1));
+ // scrolling to max scroll point
+ const maxChange: Position = getClosestScrollable(scrollable).scroll.max;
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('over foreign list', () => {
+ // $ExpectError - using spread
+ const foreign: DroppableDimension = {
+ ...scrollable,
+ descriptor: preset.foreign.descriptor,
+ };
+ const placeholder: Position = patch(
+ axis.line,
+ preset.inHome1.placeholder.withoutMargin[axis.size],
+ );
+ const overForeign: DragImpact = {
+ movement: noMovement,
+ direction: foreign.axis.direction,
+ destination: {
+ index: 0,
+ droppableId: foreign.descriptor.id,
+ },
+ };
+
+ it('should allow scrolling up to the end of the frame + the size of the placeholder', () => {
+ // scrolling to just before the end of the placeholder
+ // this goes beyond the usual max scroll.
+ const scroll: Position = add(
+ // usual max scroll
+ getClosestScrollable(foreign).scroll.max,
+ // with a small bit of room towards the end of the placeholder space
+ subtract(placeholder, patch(axis.line, 1))
+ );
+ const scrolledForeign: DroppableDimension = scrollDroppable(foreign, scroll);
+ const target: Position = add(onEndOfFrame, placeholder);
+ const expected: Position = patch(axis.line, config.maxScrollSpeed);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target, overForeign), scrolledForeign),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(foreign.descriptor.id, expected);
+ });
+
+ it('should not allow scrolling past the placeholder buffer', () => {
+ // already on the placeholder
+ const scroll: Position = add(
+ // usual max scroll
+ getClosestScrollable(foreign).scroll.max,
+ // with the placeholder
+ placeholder,
+ );
+ const scrolledForeign: DroppableDimension = scrollDroppable(foreign, scroll);
+ // targeting beyond the placeholder
+ const target: Position = add(
+ add(onEndOfFrame, placeholder),
+ patch(axis.line, 1),
+ );
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target, overForeign), scrolledForeign),
+ );
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('moving backward to the start of droppable', () => {
+ const droppableScroll: Position = patch(axis.line, 10);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll);
+
+ const onStartBoundary: Position = patch(
+ axis.line,
+ // to the boundary is not enough to start
+ (frame[axis.start] + thresholds.startFrom),
+ frame.center[axis.crossAxisLine],
+ );
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (frame[axis.start] + thresholds.maxSpeedAt),
+ frame.center[axis.crossAxisLine],
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onStartBoundary), scrolled)
+ );
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ // going backwards
+ const target: Position = subtract(onStartBoundary, patch(axis.line, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalled();
+ const [id, scroll] = mocks.scrollDroppable.mock.calls[0];
+
+ // validation
+ expect(id).toBe(scrollable.descriptor.id);
+ // moving backwards
+ expect(scroll[axis.line]).toBeLessThan(0);
+ expect(scroll[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should throttle multiple scrolls into a single animation frame', () => {
+ const target1: Position = subtract(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = subtract(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target1), scrolled),
+ );
+ autoScroller.onStateChange(
+ addDroppable(dragTo(target1), scrolled),
+ addDroppable(dragTo(target2), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+
+ // verification
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+
+ // not testing value called as we are not exposing getRequired scroll
+ });
+
+ it('should get faster the closer to the max speed point', () => {
+ const target1: Position = subtract(onStartBoundary, patch(axis.line, 1));
+ const target2: Position = subtract(onStartBoundary, patch(axis.line, 2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target1), scrolled),
+ );
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1);
+ const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any);
+
+ autoScroller.onStateChange(
+ addDroppable(dragTo(target1), scrolled),
+ addDroppable(dragTo(target2), scrolled),
+ );
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2);
+ const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any);
+
+ // moving backwards
+ expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]);
+
+ // validation
+ expect(scroll1[axis.crossAxisLine]).toBe(0);
+ expect(scroll2[axis.crossAxisLine]).toBe(0);
+ });
+
+ it('should have the top speed at the max speed point', () => {
+ const expected: Position = patch(axis.line, -config.maxScrollSpeed);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onMaxBoundary), scrolled),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ expected
+ );
+ });
+
+ it('should have the top speed when moving beyond the max speed point', () => {
+ const target: Position = subtract(onMaxBoundary, patch(axis.line, 1));
+ const expected: Position = patch(axis.line, -config.maxScrollSpeed);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ expected
+ );
+ });
+
+ it('should not scroll if the item is too big', () => {
+ const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 }));
+ const tooBig: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'too big',
+ droppableId: preset.home.descriptor.id,
+ // after the last item
+ index: preset.inHomeList.length,
+ },
+ client: expanded,
+ });
+ const selection: Position = onMaxBoundary;
+ const custom: State = (() => {
+ const base: State = state.dragging(
+ preset.inHome1.descriptor.id,
+ selection,
+ );
+
+ const updated: State = {
+ ...base,
+ drag: {
+ ...base.drag,
+ initial: {
+ // $ExpectError
+ ...base.drag.initial,
+ descriptor: tooBig.descriptor,
+ },
+ },
+ };
+
+ return addDroppable(addDraggable(updated, tooBig), scrolled);
+ })();
+
+ autoScroller.onStateChange(state.idle, custom);
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ it('should not scroll if the droppable is unable to be scrolled', () => {
+ const target: Position = onMaxBoundary;
+ if (!scrollable.viewport.closestScrollable) {
+ throw new Error('Invalid test setup');
+ }
+ // scrolling to max scroll point
+
+ autoScroller.onStateChange(
+ state.idle,
+ // scrollable cannot be scrolled backwards
+ addDroppable(dragTo(target), scrollable)
+ );
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+
+ // just some light tests to ensure that cross axis moving also works
+ describe('moving forward on the cross axis', () => {
+ const droppableScroll: Position = patch(axis.crossAxisLine, 10);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll);
+
+ const onStartBoundary: Position = patch(
+ axis.line,
+ frame.center[axis.line],
+ // to the boundary is not enough to start
+ (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom),
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(state.idle, dragTo(onStartBoundary));
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalled();
+ // moving forwards
+ const [id, scroll] = mocks.scrollDroppable.mock.calls[0];
+
+ expect(id).toBe(scrolled.descriptor.id);
+ expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0);
+ });
+ });
+
+ // just some light tests to ensure that cross axis moving also works
+ describe('moving backward on the cross axis', () => {
+ const droppableScroll: Position = patch(axis.crossAxisLine, 10);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll);
+
+ const onStartBoundary: Position = patch(
+ axis.line,
+ frame.center[axis.line],
+ // to the boundary is not enough to start
+ (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom)
+ );
+
+ it('should not scroll if not past the start threshold', () => {
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onStartBoundary), scrolled),
+ );
+
+ requestAnimationFrame.flush();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+
+ it('should scroll if moving beyond the start threshold', () => {
+ const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(target), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // only called after a frame
+ requestAnimationFrame.step();
+ expect(mocks.scrollDroppable).toHaveBeenCalled();
+ // moving backwards
+ const request: Position = mocks.scrollDroppable.mock.calls[0][1];
+ expect(request[axis.crossAxisLine]).toBeLessThan(0);
+ });
+ });
+
+ describe('over frame but not a subject', () => {
+ const withSmallSubject: DroppableDimension = getDroppableDimension({
+ // stealing the home descriptor
+ descriptor: preset.home.descriptor,
+ direction: axis.direction,
+ client: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 5000,
+ bottom: 5000,
+ }),
+ scrollWidth: 10000,
+ scrollHeight: 10000,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ const endOfSubject: Position = patch(axis.line, 100);
+ const endOfFrame: Position = patch(
+ axis.line,
+ // on the end
+ 5000,
+ // half way
+ 2500,
+ );
+
+ it('should scroll a frame if it is being dragged over, even if not over the subject', () => {
+ const scrolled: DroppableDimension = scrollDroppable(
+ withSmallSubject,
+ // scrolling the whole client away
+ endOfSubject,
+ );
+ // subject no longer visible
+ expect(scrolled.viewport.clipped).toBe(null);
+ // const target: Position = add(endOfFrame, patch(axis.line, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ withImpact(
+ addDroppable(dragTo(endOfFrame), scrolled),
+ // being super clear that we are not currently over any droppable
+ noImpact,
+ )
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ maxScrollSpeed,
+ );
+ });
+
+ it('should not scroll the frame if not over the frame', () => {
+ const scrolled: DroppableDimension = scrollDroppable(
+ withSmallSubject,
+ // scrolling the whole client away
+ endOfSubject,
+ );
+ // subject no longer visible
+ expect(scrolled.viewport.clipped).toBe(null);
+ const target: Position = add(endOfFrame, patch(axis.line, 1));
+
+ autoScroller.onStateChange(
+ state.idle,
+ withImpact(
+ addDroppable(dragTo(target), scrolled),
+ // being super clear that we are not currently over any droppable
+ noImpact,
+ )
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('window scrolling before droppable scrolling', () => {
+ const custom: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'scrollable that is similiar to the viewport',
+ type: 'TYPE',
+ },
+ client: getArea({
+ top: 0,
+ left: 0,
+ // bigger than the frame
+ right: windowScrollSize.scrollWidth,
+ bottom: windowScrollSize.scrollHeight,
+ }),
+ closest: {
+ frameClient: viewport,
+ scrollWidth: windowScrollSize.scrollWidth,
+ scrollHeight: windowScrollSize.scrollHeight,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+ const thresholds: PixelThresholds = getPixelThresholds(viewport, axis);
+
+ it('should scroll the window only if both the window and droppable can be scrolled', () => {
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (viewport[axis.size] - thresholds.maxSpeedAt),
+ viewport.center[axis.crossAxisLine],
+ );
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(dragTo(onMaxBoundary), custom),
+ );
+ requestAnimationFrame.step();
+
+ expect(mocks.scrollWindow).toHaveBeenCalled();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('on drag end', () => {
+ const endDragStates = [
+ state.idle,
+ state.dropAnimating(),
+ state.userCancel(),
+ state.dropComplete(),
+ ];
+
+ endDragStates.forEach((end: State) => {
+ it('should cancel any pending window scroll', () => {
+ const thresholds: PixelThresholds = getPixelThresholds(viewport, axis);
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (viewport[axis.size] - thresholds.maxSpeedAt),
+ viewport.center[axis.crossAxisLine],
+ );
+
+ autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary));
+
+ // frame not cleared
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+
+ // should cancel the next frame
+ autoScroller.onStateChange(dragTo(onMaxBoundary), end);
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should cancel any pending droppable scroll', () => {
+ const thresholds: PixelThresholds = getPixelThresholds(frame, axis);
+ const onMaxBoundary: Position = patch(
+ axis.line,
+ (frame[axis.size] - thresholds.maxSpeedAt),
+ frame.center[axis.crossAxisLine],
+ );
+ const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable);
+
+ autoScroller.onStateChange(
+ state.idle,
+ drag
+ );
+
+ // frame not cleared
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+
+ // should cancel the next frame
+ autoScroller.onStateChange(drag, end);
+ requestAnimationFrame.flush();
+
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js
new file mode 100644
index 0000000000..6a540e6382
--- /dev/null
+++ b/test/unit/state/auto-scroll/jump-scroller.spec.js
@@ -0,0 +1,550 @@
+// @flow
+import type {
+ Area,
+ Axis,
+ Position,
+ State,
+ DroppableDimension,
+} from '../../../../src/types';
+import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types';
+import { add, patch, subtract, negate } from '../../../../src/state/position';
+import getArea from '../../../../src/state/get-area';
+import setViewport, { resetViewport } from '../../../utils/set-viewport';
+import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size';
+import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll';
+import { vertical, horizontal } from '../../../../src/state/axis';
+import createAutoScroller from '../../../../src/state/auto-scroller';
+import getStatePreset from '../../../utils/get-simple-state-preset';
+import { getPreset, addDroppable } from '../../../utils/dimension';
+import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
+import getMaxScroll from '../../../../src/state/get-max-scroll';
+
+const origin: Position = { x: 0, y: 0 };
+
+const windowScrollSize = {
+ scrollHeight: 2000,
+ scrollWidth: 1600,
+};
+const viewport: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 800,
+ bottom: 1000,
+});
+
+const disableWindowScroll = () => {
+ setWindowScrollSize({
+ scrollHeight: viewport.height,
+ scrollWidth: viewport.width,
+ });
+};
+
+describe('jump auto scrolling', () => {
+ let autoScroller: AutoScroller;
+ let mocks;
+
+ beforeEach(() => {
+ mocks = {
+ scrollWindow: jest.fn(),
+ scrollDroppable: jest.fn(),
+ move: jest.fn(),
+ };
+ autoScroller = createAutoScroller(mocks);
+ setViewport(viewport);
+ setWindowScrollSize(windowScrollSize);
+ });
+
+ afterEach(() => {
+ resetWindowScroll();
+ resetWindowScrollSize();
+ resetViewport();
+ requestAnimationFrame.reset();
+ });
+
+ [vertical, horizontal].forEach((axis: Axis) => {
+ describe(`on the ${axis.direction} axis`, () => {
+ const preset = getPreset(axis);
+ const state = getStatePreset(axis);
+
+ describe('window scrolling', () => {
+ describe('moving forwards', () => {
+ it('should manually move the item if the window is unable to scroll', () => {
+ disableWindowScroll();
+ const request: Position = patch(axis.line, 1);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(current.drag.current.client.selection, request);
+
+ autoScroller.onStateChange(state.idle, current);
+
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ origin,
+ true,
+ );
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll the window if can absorb all of the movement', () => {
+ const request: Position = patch(axis.line, 1);
+
+ autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request));
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(request);
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+
+ it('should manually move the item any distance that the window is unable to scroll', () => {
+ // only allowing scrolling by 1 px
+ setWindowScrollSize({
+ scrollHeight: viewport.height + 1,
+ scrollWidth: viewport.width + 1,
+ });
+ // more than the 1 pixel allowed
+ const request: Position = patch(axis.line, 3);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(
+ current.drag.current.client.selection,
+ // the two pixels that could not be done by the window
+ patch(axis.line, 2)
+ );
+
+ autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request));
+
+ // can scroll with what we have
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, 1));
+ // remainder to be done by movement
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ origin,
+ true,
+ );
+ });
+ });
+
+ describe('moving backwards', () => {
+ it('should manually move the item if the window is unable to scroll', () => {
+ // unable to scroll backwards to start with
+ const request: Position = patch(axis.line, -1);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(current.drag.current.client.selection, request);
+
+ autoScroller.onStateChange(state.idle, current);
+
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ origin,
+ true,
+ );
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ });
+
+ it('should scroll the window if can absorb all of the movement', () => {
+ setWindowScroll(patch(axis.line, 1));
+ const request: Position = patch(axis.line, -1);
+
+ autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request));
+
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(request);
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+
+ it('should manually move the item any distance that the window is unable to scroll', () => {
+ // only allowing scrolling by 1 px
+ const windowScroll: Position = patch(axis.line, 1);
+ setWindowScroll(windowScroll);
+ // more than the 1 pixel allowed
+ const request: Position = patch(axis.line, -3);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(
+ current.drag.current.client.selection,
+ // the two pixels that could not be done by the window
+ patch(axis.line, -2)
+ );
+
+ autoScroller.onStateChange(state.idle, current);
+
+ // can scroll with what we have
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, -1));
+ // remainder to be done by movement
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ windowScroll,
+ true,
+ );
+ });
+ });
+ });
+
+ describe('droppable scrolling (which can involve some window scrolling)', () => {
+ const scrollableScrollSize = {
+ scrollWidth: 800,
+ scrollHeight: 800,
+ };
+ const frame: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 600,
+ bottom: 600,
+ });
+ const scrollable: DroppableDimension = getDroppableDimension({
+ // stealing the home descriptor so that the dragging item will
+ // be within in
+ descriptor: preset.home.descriptor,
+ client: getArea({
+ top: 0,
+ left: 0,
+ // bigger than the frame
+ right: scrollableScrollSize.scrollWidth,
+ bottom: scrollableScrollSize.scrollHeight,
+ }),
+ closest: {
+ frameClient: frame,
+ scrollWidth: scrollableScrollSize.scrollWidth,
+ scrollHeight: scrollableScrollSize.scrollHeight,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ if (!scrollable.viewport.closestScrollable) {
+ throw new Error('Invalid droppable');
+ }
+
+ const maxDroppableScroll: Position =
+ scrollable.viewport.closestScrollable.scroll.max;
+
+ describe('moving forwards', () => {
+ describe('droppable is able to complete entire scroll', () => {
+ it('should only scroll the droppable', () => {
+ const request: Position = patch(axis.line, 1);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(state.scrollJumpRequest(request), scrollable),
+ );
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrollable.descriptor.id,
+ request,
+ );
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('droppable is unable to complete the entire scroll', () => {
+ it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => {
+ // droppable can no longer be scrolled
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ maxDroppableScroll,
+ );
+ disableWindowScroll();
+ const request: Position = patch(axis.line, 1);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(current.drag.current.client.selection, request);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(current, scrolled),
+ );
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ origin,
+ true,
+ );
+ });
+
+ describe('window is unable to absorb some of the scroll', () => {
+ beforeEach(() => {
+ disableWindowScroll();
+ });
+
+ it('should scroll the droppable what it can and move the rest', () => {
+ // able to scroll 1 pixel forward
+ const availableScroll: Position = patch(axis.line, 1);
+ const scroll: Position = subtract(maxDroppableScroll, availableScroll);
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ scroll,
+ );
+ // want to move 3 pixels
+ const request: Position = patch(axis.line, 3);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expectedManualMove: Position =
+ add(current.drag.current.client.selection, patch(axis.line, 2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(current, scrolled),
+ );
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ preset.home.descriptor.id,
+ availableScroll,
+ );
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expectedManualMove,
+ origin,
+ true,
+ );
+ });
+ });
+
+ describe('window can absorb some of the scroll', () => {
+ it('should scroll the entire overlap if it can', () => {
+ const availableScroll: Position = patch(axis.line, 1);
+ const scroll: Position = subtract(maxDroppableScroll, availableScroll);
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ scroll,
+ );
+ // want to move 3 pixels
+ const request: Position = patch(axis.line, 3);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(state.scrollJumpRequest(request), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ availableScroll,
+ );
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, 2));
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+
+ it('should scroll the droppable and window by what it can, and manually move the rest', () => {
+ // Setting the window scroll so it has a small amount of available space
+ const availableWindowScroll: Position = patch(axis.line, 2);
+ const maxWindowScroll: Position = getMaxScroll({
+ scrollHeight: windowScrollSize.scrollHeight,
+ scrollWidth: windowScrollSize.scrollWidth,
+ height: viewport.height,
+ width: viewport.width,
+ });
+ const windowScroll: Position = subtract(maxWindowScroll, availableWindowScroll);
+ setWindowScroll(windowScroll);
+ // Setting the droppable scroll so it has a small amount of available space
+ const availableDroppableScroll: Position = patch(axis.line, 1);
+ const droppableScroll: Position = subtract(
+ maxDroppableScroll,
+ availableDroppableScroll
+ );
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ droppableScroll,
+ );
+ // How much we want to scroll
+ const request: Position = patch(axis.line, 5);
+ // How much we will not be able to absorb with droppable and window scroll
+ const remainder: Position =
+ subtract(subtract(request, availableDroppableScroll), availableWindowScroll);
+ const current = addDroppable(state.scrollJumpRequest(request), scrolled);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expectedManualMove: Position =
+ add(current.drag.current.client.selection, remainder);
+
+ autoScroller.onStateChange(state.idle, current);
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ availableDroppableScroll,
+ );
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(availableWindowScroll);
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expectedManualMove,
+ windowScroll,
+ true,
+ );
+ });
+ });
+ });
+ });
+
+ describe('moving backwards', () => {
+ describe('droppable is able to complete entire scroll', () => {
+ it('should only scroll the droppable', () => {
+ // move forward slightly to allow us to move forwards
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 1));
+ const request: Position = patch(axis.line, -1);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(state.scrollJumpRequest(request), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ request,
+ );
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('droppable is unable to complete the entire scroll', () => {
+ it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => {
+ // scrollable cannot scroll backwards by default
+ disableWindowScroll();
+ const request: Position = patch(axis.line, -1);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expected: Position = add(current.drag.current.client.selection, request);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(current, scrollable),
+ );
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.scrollDroppable).not.toHaveBeenCalled();
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expected,
+ origin,
+ true,
+ );
+ });
+
+ describe('window is unable to absorb some of the scroll', () => {
+ beforeEach(() => {
+ disableWindowScroll();
+ });
+
+ it('should scroll the droppable what it can and move the rest', () => {
+ // able to scroll 1 pixel forward
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ patch(axis.line, 1),
+ );
+ // want to move backwards 3 pixels
+ const request: Position = patch(axis.line, -3);
+ const current: State = state.scrollJumpRequest(request);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ // manual move will take what the droppable cannot
+ const expectedManualMove: Position =
+ add(current.drag.current.client.selection, patch(axis.line, -2));
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(current, scrolled),
+ );
+
+ expect(mocks.scrollWindow).not.toHaveBeenCalled();
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ preset.home.descriptor.id,
+ // can only scroll backwards what it has!
+ patch(axis.line, -1),
+ );
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expectedManualMove,
+ origin,
+ true,
+ );
+ });
+ });
+
+ describe('window can absorb some of the scroll', () => {
+ it('should scroll the entire overlap if it can', () => {
+ // let the window scroll be enough to move back into
+ setWindowScroll(patch(axis.line, 100));
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ patch(axis.line, 1),
+ );
+ // want to move 3 pixels backwards
+ const request: Position = patch(axis.line, -3);
+
+ autoScroller.onStateChange(
+ state.idle,
+ addDroppable(state.scrollJumpRequest(request), scrolled),
+ );
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ patch(axis.line, -1),
+ );
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, -2));
+ expect(mocks.move).not.toHaveBeenCalled();
+ });
+
+ it('should scroll the droppable and window by what it can, and manually move the rest', () => {
+ // Setting the window scroll so it has a small amount of available space
+ const windowScroll: Position = patch(axis.line, 2);
+ setWindowScroll(windowScroll);
+ // Setting the droppable scroll so it has a small amount of available space
+ const droppableScroll: Position = patch(axis.line, 1);
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ droppableScroll,
+ );
+ // How much we want to scroll
+ const request: Position = patch(axis.line, -5);
+ // How much we will not be able to absorb with droppable and window scroll
+ const remainder: Position = patch(axis.line, -2);
+ const current = addDroppable(state.scrollJumpRequest(request), scrolled);
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ const expectedManualMove: Position =
+ add(current.drag.current.client.selection, remainder);
+
+ autoScroller.onStateChange(state.idle, current);
+
+ expect(mocks.scrollDroppable).toHaveBeenCalledWith(
+ scrolled.descriptor.id,
+ negate(droppableScroll),
+ );
+ expect(mocks.scrollWindow).toHaveBeenCalledWith(negate(windowScroll));
+ expect(mocks.move).toHaveBeenCalledWith(
+ preset.inHome1.descriptor.id,
+ expectedManualMove,
+ windowScroll,
+ true,
+ );
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/state/can-start-drag.spec.js b/test/unit/state/can-start-drag.spec.js
index a4de7a0157..8718e110ca 100644
--- a/test/unit/state/can-start-drag.spec.js
+++ b/test/unit/state/can-start-drag.spec.js
@@ -1,10 +1,11 @@
// @flow
import canStartDrag from '../../../src/state/can-start-drag';
-import * as state from '../../utils/simple-state-preset';
+import getStatePreset from '../../utils/get-simple-state-preset';
import { getPreset } from '../../utils/dimension';
import type { State } from '../../../src/types';
const preset = getPreset();
+const state = getStatePreset();
describe('can start drag', () => {
describe('at rest', () => {
diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js
index 3fbbdd2c0e..b7ce15f5df 100644
--- a/test/unit/state/dimension.spec.js
+++ b/test/unit/state/dimension.spec.js
@@ -6,9 +6,11 @@ import {
clip,
} from '../../../src/state/dimension';
import { vertical, horizontal } from '../../../src/state/axis';
-import { offset } from '../../../src/state/spacing';
+import { offsetByPosition } from '../../../src/state/spacing';
import getArea from '../../../src/state/get-area';
import { negate } from '../../../src/state/position';
+import getMaxScroll from '../../../src/state/get-max-scroll';
+import { getClosestScrollable } from '../../utils/dimension';
import type {
Area,
Spacing,
@@ -17,6 +19,7 @@ import type {
Position,
DraggableDimension,
DroppableDimension,
+ ClosestScrollable,
} from '../../../src/types';
const droppableDescriptor: DroppableDescriptor = {
@@ -47,28 +50,6 @@ const windowScroll: Position = {
};
const origin: Position = { x: 0, y: 0 };
-const addPosition = (area: Area, point: Position): Area => {
- const { top, right, bottom, left } = area;
- return getArea({
- top: top + point.y,
- left: left + point.x,
- bottom: bottom + point.y,
- right: right + point.x,
- });
-};
-
-const addSpacing = (area: Area, spacing: Spacing): Area => {
- const { top, right, bottom, left } = area;
- return getArea({
- // pulling back to increase size
- top: top - spacing.top,
- left: left - spacing.left,
- // pushing forward to increase size
- bottom: bottom + spacing.bottom,
- right: right + spacing.right,
- });
-};
-
describe('dimension', () => {
describe('draggable dimension', () => {
const dimension: DraggableDimension = getDraggableDimension({
@@ -128,29 +109,12 @@ describe('dimension', () => {
});
describe('droppable dimension', () => {
- const frameScroll: Position = {
- x: 10,
- y: 20,
- };
-
const dimension: DroppableDimension = getDroppableDimension({
descriptor: droppableDescriptor,
client,
margin,
padding,
windowScroll,
- frameScroll,
- });
-
- it('should return the initial scroll as the initial and current scroll', () => {
- expect(dimension.viewport.frameScroll).toEqual({
- initial: frameScroll,
- current: frameScroll,
- diff: {
- value: origin,
- displacement: origin,
- },
- });
});
it('should apply the correct axis', () => {
@@ -159,14 +123,12 @@ describe('dimension', () => {
client,
margin,
windowScroll,
- frameScroll,
});
const withVertical: DroppableDimension = getDroppableDimension({
descriptor: droppableDescriptor,
client,
margin,
windowScroll,
- frameScroll,
direction: 'vertical',
});
const withHorizontal: DroppableDimension = getDroppableDimension({
@@ -174,7 +136,6 @@ describe('dimension', () => {
client,
margin,
windowScroll,
- frameScroll,
direction: 'horizontal',
});
@@ -186,7 +147,7 @@ describe('dimension', () => {
expect(withHorizontal.axis).toBe(horizontal);
});
- describe('without scroll (client)', () => {
+ describe('without window scroll (client)', () => {
it('should return a portion that does not consider margins', () => {
const area: Area = getArea({
top: client.top,
@@ -221,7 +182,7 @@ describe('dimension', () => {
});
});
- describe('with scroll (page)', () => {
+ describe('with window scroll (page)', () => {
it('should return a portion that does not consider margins', () => {
const area: Area = getArea({
top: client.top + windowScroll.y,
@@ -248,79 +209,115 @@ describe('dimension', () => {
const area: Area = getArea({
top: (client.top + windowScroll.y) - margin.top - padding.top,
left: (client.left + windowScroll.x) - margin.left - padding.left,
- bottom: client.bottom + windowScroll.y + margin.bottom + padding.bottom,
- right: client.right + windowScroll.x + margin.right + padding.right,
+ bottom: (client.bottom + windowScroll.y) + margin.bottom + padding.bottom,
+ right: (client.right + windowScroll.x) + margin.right + padding.right,
});
expect(dimension.page.withMarginAndPadding).toEqual(area);
});
});
- describe('viewport', () => {
- it('should use the area as the frame if no frame is provided', () => {
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: droppableDescriptor,
- client,
- margin,
- windowScroll: origin,
- frameScroll,
+ describe('closest scrollable', () => {
+ describe('basic info about the scrollable', () => {
+ // eslint-disable-next-line no-shadow
+ const client: Area = getArea({
+ top: 0,
+ right: 300,
+ bottom: 300,
+ left: 0,
+ });
+ const frameClient: Area = getArea({
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
});
- expect(droppable.viewport.frame).toEqual(addSpacing(client, margin));
- });
-
- it('should include the window scroll', () => {
- const droppable: DroppableDimension = getDroppableDimension({
+ const withScrollable: DroppableDimension = getDroppableDimension({
descriptor: droppableDescriptor,
client,
- margin,
windowScroll,
- frameScroll,
+ closest: {
+ frameClient,
+ scrollWidth: 500,
+ scrollHeight: 500,
+ scroll: { x: 10, y: 10 },
+ shouldClipSubject: true,
+ },
});
- expect(droppable.viewport.frame).toEqual(
- addPosition(addSpacing(client, margin), windowScroll),
- );
- });
+ it('should not have a closest scrollable if there is no closest scrollable', () => {
+ const noClosestScrollable: DroppableDimension = getDroppableDimension({
+ descriptor: droppableDescriptor,
+ client,
+ });
- it('should use the frameClient as the frame if provided', () => {
- const frameClient: Area = getArea({
- top: 20,
- left: 30,
- right: 40,
- bottom: 50,
+ expect(noClosestScrollable.viewport.closestScrollable).toBe(null);
+ expect(noClosestScrollable.viewport.subject)
+ .toEqual(noClosestScrollable.viewport.clipped);
});
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: droppableDescriptor,
- client,
- frameClient,
+ it('should offset the frame client by the window scroll', () => {
+ expect(getClosestScrollable(withScrollable).frame).toEqual(
+ getArea(offsetByPosition(frameClient, windowScroll))
+ );
});
- expect(droppable.viewport.frame).toEqual(frameClient);
+ it('should set the max scroll point for the closest scrollable', () => {
+ expect(getClosestScrollable(withScrollable).scroll.max).toEqual({ x: 400, y: 400 });
+ });
});
describe('frame clipping', () => {
+ const frameClient = getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ });
+ const getWithClient = (subject: Area): DroppableDimension => getDroppableDimension({
+ descriptor: droppableDescriptor,
+ client: subject,
+ closest: {
+ frameClient,
+ scrollWidth: 300,
+ scrollHeight: 300,
+ scroll: origin,
+ shouldClipSubject: true,
+ },
+ });
+
+ it('should not clip the frame if requested not to', () => {
+ const withoutClipping: DroppableDimension = getDroppableDimension({
+ descriptor: droppableDescriptor,
+ client,
+ windowScroll,
+ closest: {
+ frameClient,
+ scrollWidth: 300,
+ scrollHeight: 300,
+ scroll: origin,
+ // disabling clipping
+ shouldClipSubject: false,
+ },
+ });
+
+ expect(withoutClipping.viewport.subject).toEqual(withoutClipping.viewport.clipped);
+
+ expect(getClosestScrollable(withoutClipping).shouldClipSubject).toBe(false);
+ });
+
describe('frame is smaller than subject', () => {
it('should clip the subject to the size of the frame', () => {
const subject = getArea({
top: 0,
- right: 100,
- bottom: 100,
left: 0,
- });
- const frameClient = getArea({
- top: 10,
- right: 90,
- bottom: 90,
- left: 10,
+ // 100px bigger than the frame on the bottom and right
+ bottom: 200,
+ right: 200,
});
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: droppableDescriptor,
- client: subject,
- frameClient,
- });
+ const droppable: DroppableDimension = getWithClient(subject);
expect(droppable.viewport.clipped).toEqual(frameClient);
});
@@ -328,12 +325,7 @@ describe('dimension', () => {
describe('frame is larger than subject', () => {
it('should return a clipped size that is equal to that of the subject', () => {
- const frameClient = getArea({
- top: 0,
- right: 100,
- bottom: 100,
- left: 0,
- });
+ // 10px smaller on every side
const subject = getArea({
top: 10,
right: 90,
@@ -341,24 +333,13 @@ describe('dimension', () => {
left: 10,
});
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: droppableDescriptor,
- client: subject,
- frameClient,
- });
+ const droppable: DroppableDimension = getWithClient(subject);
expect(droppable.viewport.clipped).toEqual(subject);
});
});
describe('subject clipped on one side by frame', () => {
- const frameClient = getArea({
- top: 0,
- right: 100,
- bottom: 100,
- left: 0,
- });
-
it('should clip on all sides', () => {
// each of these subjects bleeds out past the frame in one direction
const subjects: Area[] = [
@@ -381,11 +362,7 @@ describe('dimension', () => {
];
subjects.forEach((subject: Area) => {
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: droppableDescriptor,
- client: subject,
- frameClient,
- });
+ const droppable: DroppableDimension = getWithClient(subject);
expect(droppable.viewport.clipped).toEqual(frameClient);
});
@@ -397,49 +374,67 @@ describe('dimension', () => {
describe('scrolling a droppable', () => {
it('should update the frame scroll and the clipping', () => {
+ const scrollSize = {
+ scrollHeight: 500,
+ scrollWidth: 100,
+ };
const subject = getArea({
// 500 px high
top: 0,
- bottom: 500,
- right: 100,
+ bottom: scrollSize.scrollHeight,
+ right: scrollSize.scrollWidth,
left: 0,
});
const frameClient = getArea({
// only viewing top 100px
- top: 0,
bottom: 100,
// unchanged
- right: 100,
+ top: 0,
+ right: scrollSize.scrollWidth,
left: 0,
});
const frameScroll: Position = { x: 0, y: 0 };
const droppable: DroppableDimension = getDroppableDimension({
descriptor: droppableDescriptor,
client: subject,
- frameClient,
- frameScroll,
+ closest: {
+ frameClient,
+ scroll: frameScroll,
+ scrollWidth: scrollSize.scrollWidth,
+ scrollHeight: scrollSize.scrollHeight,
+ shouldClipSubject: true,
+ },
});
+ const closestScrollable: ClosestScrollable = getClosestScrollable(droppable);
+
// original frame
- expect(droppable.viewport.frame).toEqual(frameClient);
+ expect(closestScrollable.frame).toEqual(frameClient);
// subject is currently clipped by the frame
expect(droppable.viewport.clipped).toEqual(frameClient);
// scrolling down
const newScroll: Position = { x: 0, y: 100 };
const updated: DroppableDimension = scrollDroppable(droppable, newScroll);
+ const updatedClosest: ClosestScrollable = getClosestScrollable(updated);
// unchanged frame client
- expect(updated.viewport.frame).toEqual(frameClient);
+ expect(updatedClosest.frame).toEqual(frameClient);
// updated scroll info
- expect(updated.viewport.frameScroll).toEqual({
+ expect(updatedClosest.scroll).toEqual({
initial: frameScroll,
current: newScroll,
diff: {
value: newScroll,
displacement: negate(newScroll),
},
+ max: getMaxScroll({
+ scrollWidth: scrollSize.scrollWidth,
+ scrollHeight: scrollSize.scrollHeight,
+ width: frameClient.width,
+ height: frameClient.height,
+ }),
});
// updated clipped
@@ -452,6 +447,40 @@ describe('dimension', () => {
left: 0,
}));
});
+
+ it('should allow scrolling beyond the max position', () => {
+ // this is to allow for scrolling into a foreign placeholder
+ const scrollable: DroppableDimension = getDroppableDimension({
+ descriptor: droppableDescriptor,
+ client: getArea({
+ top: 0,
+ left: 0,
+ right: 200,
+ bottom: 200,
+ }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ scroll: { x: 0, y: 0 },
+ scrollWidth: 200,
+ scrollHeight: 200,
+ shouldClipSubject: true,
+ },
+ });
+
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, { x: 300, y: 300 });
+
+ // current is larger than max
+ expect(getClosestScrollable(scrolled).scroll.current).toEqual({ x: 300, y: 300 });
+ // current max is unchanged
+ expect(getClosestScrollable(scrolled).scroll.max).toEqual({ x: 100, y: 100 });
+ // original max
+ expect(getClosestScrollable(scrollable).scroll.max).toEqual({ x: 100, y: 100 });
+ });
});
describe('subject clipping', () => {
@@ -481,13 +510,13 @@ describe('dimension', () => {
});
const outside: Spacing[] = [
// top
- offset(frame, { x: 0, y: -200 }),
+ offsetByPosition(frame, { x: 0, y: -200 }),
// right
- offset(frame, { x: 200, y: 0 }),
+ offsetByPosition(frame, { x: 200, y: 0 }),
// bottom
- offset(frame, { x: 0, y: 200 }),
+ offsetByPosition(frame, { x: 0, y: 200 }),
// left
- offset(frame, { x: -200, y: 0 }),
+ offsetByPosition(frame, { x: -200, y: 0 }),
];
outside.forEach((subject: Spacing) => {
diff --git a/test/unit/state/fire-hooks.spec.js b/test/unit/state/fire-hooks.spec.js
deleted file mode 100644
index e61a9963a8..0000000000
--- a/test/unit/state/fire-hooks.spec.js
+++ /dev/null
@@ -1,273 +0,0 @@
-// @flow
-import fireHooks from '../../../src/state/fire-hooks';
-import * as state from '../../utils/simple-state-preset';
-import { getPreset } from '../../utils/dimension';
-import type {
- DropResult,
- Hooks,
- State,
- DimensionState,
- DraggableLocation,
- DragStart,
-} from '../../../src/types';
-
-const preset = getPreset();
-
-const noDimensions: DimensionState = {
- request: null,
- draggable: {},
- droppable: {},
-};
-
-describe('fire hooks', () => {
- let hooks: Hooks;
-
- beforeEach(() => {
- hooks = {
- onDragStart: jest.fn(),
- onDragEnd: jest.fn(),
- };
- jest.spyOn(console, 'error').mockImplementation(() => { });
- });
-
- afterEach(() => {
- console.error.mockRestore();
- });
-
- describe('drag start', () => {
- it('should call the onDragStart hook when a drag starts', () => {
- fireHooks(hooks, state.requesting(), state.dragging());
- const expected: DragStart = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- source: {
- droppableId: preset.inHome1.descriptor.droppableId,
- index: preset.inHome1.descriptor.index,
- },
- };
-
- expect(hooks.onDragStart).toHaveBeenCalledWith(expected);
- });
-
- it('should do nothing if no onDragStart is not provided', () => {
- const customHooks: Hooks = {
- onDragEnd: jest.fn(),
- };
-
- fireHooks(customHooks, state.requesting(), state.dragging());
-
- expect(console.error).not.toHaveBeenCalled();
- });
-
- it('should log an error and not call the callback if there is no current drag', () => {
- const invalid: State = {
- ...state.dragging(),
- drag: null,
- };
-
- fireHooks(hooks, state.requesting(), invalid);
-
- expect(console.error).toHaveBeenCalled();
- });
-
- it('should not call if only collecting dimensions (not dragging yet)', () => {
- fireHooks(hooks, state.idle, state.preparing);
- fireHooks(hooks, state.preparing, state.requesting());
-
- expect(hooks.onDragStart).not.toHaveBeenCalled();
- });
- });
-
- describe('drag end', () => {
- // it is possible to complete a drag from a DRAGGING or DROP_ANIMATING (drop or cancel)
- const preEndStates: State[] = [
- state.dragging(),
- state.dropAnimating(),
- state.userCancel(),
- ];
-
- preEndStates.forEach((previous: State): void => {
- it('should call onDragEnd with the drop result', () => {
- const result: DropResult = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- source: {
- droppableId: preset.inHome1.descriptor.droppableId,
- index: preset.inHome1.descriptor.index,
- },
- destination: {
- droppableId: preset.inHome1.descriptor.droppableId,
- index: preset.inHome1.descriptor.index + 1,
- },
- };
- const current: State = {
- phase: 'DROP_COMPLETE',
- drop: {
- pending: null,
- result,
- },
- drag: null,
- dimension: noDimensions,
- };
-
- fireHooks(hooks, previous, current);
-
- if (!current.drop || !current.drop.result) {
- throw new Error('invalid state');
- }
-
- const provided: DropResult = current.drop.result;
- expect(hooks.onDragEnd).toHaveBeenCalledWith(provided);
- });
-
- it('should log an error and not call the callback if there is no drop result', () => {
- const invalid: State = {
- ...state.dropComplete(),
- drop: null,
- };
-
- fireHooks(hooks, previous, invalid);
-
- expect(hooks.onDragEnd).not.toHaveBeenCalled();
- expect(console.error).toHaveBeenCalled();
- });
-
- it('should call onDragEnd with null as the destination if there is no destination', () => {
- const result: DropResult = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- source: {
- droppableId: preset.inHome1.descriptor.droppableId,
- index: preset.inHome1.descriptor.index,
- },
- destination: null,
- };
- const current: State = {
- phase: 'DROP_COMPLETE',
- drop: {
- pending: null,
- result,
- },
- drag: null,
- dimension: noDimensions,
- };
-
- fireHooks(hooks, previous, current);
-
- expect(hooks.onDragEnd).toHaveBeenCalledWith(result);
- });
-
- it('should call onDragEnd with null if the item did not move', () => {
- const source: DraggableLocation = {
- droppableId: preset.inHome1.descriptor.droppableId,
- index: preset.inHome1.descriptor.index,
- };
- const result: DropResult = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- source,
- destination: source,
- };
- const current: State = {
- phase: 'DROP_COMPLETE',
- drop: {
- pending: null,
- result,
- },
- drag: null,
- dimension: noDimensions,
- };
- const expected : DropResult = {
- draggableId: result.draggableId,
- type: result.type,
- source: result.source,
- // destination has been cleared
- destination: null,
- };
-
- fireHooks(hooks, previous, current);
-
- expect(hooks.onDragEnd).toHaveBeenCalledWith(expected);
- });
- });
- });
-
- describe('drag cleared', () => {
- describe('cleared while dragging', () => {
- it('should return a result with a null destination', () => {
- const expected: DropResult = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- // $ExpectError - not checking for null
- source: {
- index: preset.inHome1.descriptor.index,
- droppableId: preset.inHome1.descriptor.droppableId,
- },
- destination: null,
- };
-
- fireHooks(hooks, state.dragging(), state.idle);
-
- expect(hooks.onDragEnd).toHaveBeenCalledWith(expected);
- });
-
- it('should log an error and do nothing if it cannot find a previous drag to publish', () => {
- const invalid: State = {
- phase: 'DRAGGING',
- drag: null,
- drop: null,
- dimension: noDimensions,
- };
-
- fireHooks(hooks, state.idle, invalid);
-
- expect(hooks.onDragEnd).not.toHaveBeenCalled();
- expect(console.error).toHaveBeenCalled();
- });
- });
-
- // this should never really happen - but just being safe
- describe('cleared while drop animating', () => {
- it('should return a result with a null destination', () => {
- const expected: DropResult = {
- draggableId: preset.inHome1.descriptor.id,
- type: preset.home.descriptor.type,
- source: {
- index: preset.inHome1.descriptor.index,
- droppableId: preset.inHome1.descriptor.droppableId,
- },
- destination: null,
- };
-
- fireHooks(hooks, state.dropAnimating(), state.idle);
-
- expect(hooks.onDragEnd).toHaveBeenCalledWith(expected);
- });
-
- it('should log an error and do nothing if it cannot find a previous drag to publish', () => {
- const invalid: State = {
- ...state.dropAnimating(),
- drop: null,
- };
-
- fireHooks(hooks, invalid, state.idle);
-
- expect(hooks.onDragEnd).not.toHaveBeenCalled();
- expect(console.error).toHaveBeenCalled();
- });
- });
- });
-
- describe('phase unchanged', () => {
- it('should not do anything if the previous and next phase are the same', () => {
- Object.keys(state).forEach((key: string) => {
- const current: State = state[key];
-
- fireHooks(hooks, current, current);
-
- expect(hooks.onDragStart).not.toHaveBeenCalled();
- expect(hooks.onDragEnd).not.toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js
index 24a9b7beb4..bd704ff3cf 100644
--- a/test/unit/state/get-drag-impact.spec.js
+++ b/test/unit/state/get-drag-impact.spec.js
@@ -13,8 +13,9 @@ import {
import {
getPreset,
disableDroppable,
+ makeScrollable,
} from '../../utils/dimension';
-import getViewport from '../../../src/state/visibility/get-viewport';
+import getViewport from '../../../src/window/get-viewport';
import type {
Axis,
DraggableDimension,
@@ -120,7 +121,7 @@ describe('get drag impact', () => {
// up to the line but not over it
inHome2.page.withoutMargin[axis.start],
// no movement on cross axis
- inHome1.page.withoutMargin.center[axis.crossLine],
+ inHome1.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
movement: {
@@ -153,7 +154,7 @@ describe('get drag impact', () => {
axis.line,
inHome4.page.withoutMargin[axis.start] + 1,
// no change
- inHome2.page.withoutMargin.center[axis.crossLine],
+ inHome2.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
movement: {
@@ -199,7 +200,7 @@ describe('get drag impact', () => {
axis.line,
inHome1.page.withoutMargin[axis.end] - 1,
// no change
- inHome3.page.withoutMargin.center[axis.crossLine],
+ inHome3.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
@@ -241,6 +242,12 @@ describe('get drag impact', () => {
});
describe('home droppable scroll has changed during a drag', () => {
+ const scrollableHome: DroppableDimension = makeScrollable(home);
+ const withScrollableHome = {
+ ...droppables,
+ [home.descriptor.id]: scrollableHome,
+ };
+
// moving inHome1 past inHome2 by scrolling the dimension
describe('moving beyond start position with own scroll', () => {
it('should move past other draggables', () => {
@@ -248,19 +255,19 @@ describe('get drag impact', () => {
const startOfInHome2: Position = patch(
axis.line,
inHome2.page.withoutMargin[axis.start],
- inHome2.page.withoutMargin.center[axis.crossLine],
+ inHome2.page.withoutMargin.center[axis.crossAxisLine],
);
const distanceNeeded: Position = add(
subtract(startOfInHome2, inHome1.page.withoutMargin.center),
// need to move over the edge
patch(axis.line, 1),
);
- const homeWithScroll: DroppableDimension = scrollDroppable(
- home, distanceNeeded
+ const scrolledHome: DroppableDimension = scrollDroppable(
+ scrollableHome, distanceNeeded
);
const updatedDroppables: DroppableDimensionMap = {
- ...droppables,
- [home.descriptor.id]: homeWithScroll,
+ ...withScrollableHome,
+ [home.descriptor.id]: scrolledHome,
};
// no changes in current page center from original
const pageCenter: Position = inHome1.page.withoutMargin.center;
@@ -302,19 +309,19 @@ describe('get drag impact', () => {
const endOfInHome2: Position = patch(
axis.line,
inHome2.page.withoutMargin[axis.end],
- inHome2.page.withoutMargin.center[axis.crossLine],
+ inHome2.page.withoutMargin.center[axis.crossAxisLine],
);
const distanceNeeded: Position = add(
subtract(endOfInHome2, inHome4.page.withoutMargin.center),
// need to move over the edge
patch(axis.line, -1),
);
- const homeWithScroll: DroppableDimension = scrollDroppable(
- home, distanceNeeded
+ const scrolledHome: DroppableDimension = scrollDroppable(
+ scrollableHome, distanceNeeded
);
const updatedDroppables: DroppableDimensionMap = {
- ...droppables,
- [home.descriptor.id]: homeWithScroll,
+ ...withScrollableHome,
+ [home.descriptor.id]: scrolledHome,
};
// no changes in current page center from original
const pageCenter: Position = inHome4.page.withoutMargin.center;
@@ -372,13 +379,19 @@ describe('get drag impact', () => {
// will be cut by the frame
[axis.end]: 200,
}),
- frameClient: getArea({
- [axis.crossAxisStart]: 0,
- [axis.crossAxisEnd]: 100,
- [axis.start]: 0,
- // will cut the subject,
- [axis.end]: 100,
- }),
+ closest: {
+ frameClient: getArea({
+ [axis.crossAxisStart]: 0,
+ [axis.crossAxisEnd]: 100,
+ [axis.start]: 0,
+ // will cut the subject,
+ [axis.end]: 100,
+ }),
+ scrollWidth: 100,
+ scrollHeight: 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const visible: DraggableDimension = getDraggableDimension({
descriptor: {
@@ -603,7 +616,7 @@ describe('get drag impact', () => {
axis.line,
// just before the end of the dimension which is the cut off
inForeign1.page.withoutMargin[axis.end] - 1,
- inForeign1.page.withoutMargin.center[axis.crossLine],
+ inForeign1.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
movement: {
@@ -660,7 +673,7 @@ describe('get drag impact', () => {
const pageCenter: Position = patch(
axis.line,
inForeign2.page.withoutMargin[axis.end] - 1,
- inForeign2.page.withoutMargin.center[axis.crossLine],
+ inForeign2.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
movement: {
@@ -712,7 +725,7 @@ describe('get drag impact', () => {
const pageCenter: Position = patch(
axis.line,
inForeign4.page.withoutMargin[axis.end],
- inForeign4.page.withoutMargin.center[axis.crossLine],
+ inForeign4.page.withoutMargin.center[axis.crossAxisLine],
);
const expected: DragImpact = {
movement: {
@@ -777,15 +790,16 @@ describe('get drag impact', () => {
const pageCenter: Position = patch(
axis.line,
inForeign2.page.withoutMargin[axis.end] - 1,
- inForeign2.page.withoutMargin.center[axis.crossLine],
+ inForeign2.page.withoutMargin.center[axis.crossAxisLine],
);
it('should have no impact impact the destination (actual)', () => {
// will go over the threshold of inForeign2 so that it will not be displaced forward
const scroll: Position = patch(axis.line, 1000);
+ const scrollableHome: DroppableDimension = makeScrollable(home, 1000);
const map: DroppableDimensionMap = {
...droppables,
- [home.descriptor.id]: scrollDroppable(home, scroll),
+ [home.descriptor.id]: scrollDroppable(scrollableHome, scroll),
};
const expected: DragImpact = {
@@ -870,10 +884,16 @@ describe('get drag impact', () => {
});
describe('destination droppable scroll is updated during a drag', () => {
+ const scrollableForeign: DroppableDimension = makeScrollable(foreign);
+ const withScrollableForeign = {
+ ...droppables,
+ [foreign.descriptor.id]: scrollableForeign,
+ };
+
const pageCenter: Position = patch(
axis.line,
inForeign2.page.withoutMargin[axis.end] - 1,
- inForeign2.page.withoutMargin.center[axis.crossLine],
+ inForeign2.page.withoutMargin.center[axis.crossAxisLine],
);
it('should impact the destination (actual)', () => {
@@ -881,8 +901,8 @@ describe('get drag impact', () => {
// be displaced forward
const scroll: Position = patch(axis.line, 1);
const map: DroppableDimensionMap = {
- ...droppables,
- [foreign.descriptor.id]: scrollDroppable(foreign, scroll),
+ ...withScrollableForeign,
+ [foreign.descriptor.id]: scrollDroppable(scrollableForeign, scroll),
};
const expected: DragImpact = {
@@ -994,6 +1014,7 @@ describe('get drag impact', () => {
const foreignCrossAxisStart: number = 120;
const foreignCrossAxisEnd: number = 200;
+
const destination: DroppableDimension = getDroppableDimension({
descriptor: {
id: 'destination',
@@ -1007,13 +1028,19 @@ describe('get drag impact', () => {
// will be cut off by the frame
[axis.end]: 200,
}),
- frameClient: getArea({
- [axis.crossAxisStart]: foreignCrossAxisStart,
- [axis.crossAxisEnd]: foreignCrossAxisEnd,
- [axis.start]: 0,
- // will cut off the subject
- [axis.end]: 100,
- }),
+ closest: {
+ frameClient: getArea({
+ [axis.crossAxisStart]: foreignCrossAxisStart,
+ [axis.crossAxisEnd]: foreignCrossAxisEnd,
+ [axis.start]: 0,
+ // will cut off the subject
+ [axis.end]: 100,
+ }),
+ scrollWidth: 100,
+ scrollHeight: 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const visible: DraggableDimension = getDraggableDimension({
descriptor: {
diff --git a/test/unit/state/get-droppable-over.spec.js b/test/unit/state/get-droppable-over.spec.js
index 40d496e94a..b8748306b1 100644
--- a/test/unit/state/get-droppable-over.spec.js
+++ b/test/unit/state/get-droppable-over.spec.js
@@ -87,10 +87,16 @@ describe('get droppable over', () => {
client: getArea({
top: 0, left: 0, right: 100, bottom: 100,
}),
- // will partially hide the subject
- frameClient: getArea({
- top: 0, left: 0, right: 50, bottom: 100,
- }),
+ closest: {
+ // will partially hide the subject
+ frameClient: getArea({
+ top: 0, left: 0, right: 50, bottom: 100,
+ }),
+ scrollHeight: 100,
+ scrollWidth: 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const draggable: DraggableDimension = getDraggableDimension({
descriptor: {
@@ -124,10 +130,17 @@ describe('get droppable over', () => {
client: getArea({
top: 0, left: 0, right: 100, bottom: 100,
}),
- // will totally hide the subject
- frameClient: getArea({
- top: 0, left: 101, right: 200, bottom: 100,
- }),
+ closest: {
+ // will partially hide the subject
+ // will totally hide the subject
+ frameClient: getArea({
+ top: 0, left: 101, right: 200, bottom: 100,
+ }),
+ scrollHeight: 100,
+ scrollWidth: 200,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const draggable: DraggableDimension = getDraggableDimension({
descriptor: {
@@ -458,12 +471,18 @@ describe('get droppable over', () => {
// cut off by the frame
bottom: 120,
}),
- frameClient: getArea({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
- }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ scrollHeight: 120,
+ scrollWidth: 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
// scrolling custom down so that it the bottom is visible
const scrolled: DroppableDimension = scrollDroppable(custom, { x: 0, y: 20 });
@@ -503,13 +522,19 @@ describe('get droppable over', () => {
// this will ensure that there is required growth in the droppable
bottom: inHome1.page.withMargin.height - 1,
}),
- frameClient: getArea({
- top: 0,
- left: 0,
- right: 100,
- // currently much bigger than client
- bottom: 500,
- }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ // currently much bigger than client
+ bottom: 500,
+ }),
+ scrollWidth: 100,
+ scrollHeight: 500,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const scrolled: DroppableDimension = scrollDroppable(foreign, { x: 0, y: 50 });
const clipped: ?Area = scrolled.viewport.clipped;
diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js
new file mode 100644
index 0000000000..ac83239f50
--- /dev/null
+++ b/test/unit/state/hook-caller.spec.js
@@ -0,0 +1,1259 @@
+// @flow
+import createHookCaller from '../../../src/state/hooks/hook-caller';
+import messagePreset from '../../../src/state/hooks/message-preset';
+import type { HookCaller } from '../../../src/state/hooks/hooks-types';
+import getStatePreset from '../../utils/get-simple-state-preset';
+import { getPreset } from '../../utils/dimension';
+import noImpact, { noMovement } from '../../../src/state/no-impact';
+import type {
+ Announce,
+ Hooks,
+ HookProvided,
+ DropResult,
+ State,
+ DimensionState,
+ DraggableLocation,
+ DraggableDescriptor,
+ DroppableDimension,
+ DragStart,
+ DragUpdate,
+ DragImpact,
+} from '../../../src/types';
+
+const preset = getPreset();
+const state = getStatePreset();
+
+const noDimensions: DimensionState = {
+ request: null,
+ draggable: {},
+ droppable: {},
+};
+
+describe('fire hooks', () => {
+ let hooks: Hooks;
+ let caller: HookCaller;
+ let announceMock: Announce;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ announceMock = jest.fn();
+ caller = createHookCaller(announceMock);
+ hooks = {
+ onDragStart: jest.fn(),
+ onDragUpdate: jest.fn(),
+ onDragEnd: jest.fn(),
+ };
+ jest.spyOn(console, 'error').mockImplementation(() => { });
+ jest.spyOn(console, 'warn').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ console.error.mockRestore();
+ console.warn.mockRestore();
+ jest.useRealTimers();
+ });
+
+ describe('drag start', () => {
+ it('should call the onDragStart hook when a drag starts', () => {
+ const expected: DragStart = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index,
+ },
+ };
+
+ caller.onStateChange(hooks, state.requesting(), state.dragging());
+
+ expect(hooks.onDragStart).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should log an error and not call the callback if there is no current drag', () => {
+ const invalid: State = {
+ ...state.dragging(),
+ drag: null,
+ };
+
+ caller.onStateChange(hooks, state.requesting(), invalid);
+
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should not call if only collecting dimensions (not dragging yet)', () => {
+ caller.onStateChange(hooks, state.idle, state.preparing);
+ caller.onStateChange(hooks, state.preparing, state.requesting());
+
+ expect(hooks.onDragStart).not.toHaveBeenCalled();
+ });
+
+ describe('announcements', () => {
+ const getDragStart = (appState: State): ?DragStart => {
+ if (!appState.drag) {
+ return null;
+ }
+
+ const descriptor: DraggableDescriptor = appState.drag.initial.descriptor;
+ const home: ?DroppableDimension = appState.dimension.droppable[descriptor.droppableId];
+
+ if (!home) {
+ return null;
+ }
+
+ const source: DraggableLocation = {
+ index: descriptor.index,
+ droppableId: descriptor.droppableId,
+ };
+
+ const start: DragStart = {
+ draggableId: descriptor.id,
+ type: home.descriptor.type,
+ source,
+ };
+
+ return start;
+ };
+
+ const dragStart: ?DragStart = getDragStart(state.dragging());
+ if (!dragStart) {
+ throw new Error('Invalid test setup');
+ }
+
+ it('should announce with the default lift message if no message is provided', () => {
+ caller.onStateChange(hooks, state.requesting(), state.dragging());
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart));
+ });
+
+ it('should announce with the default lift message if no onDragStart hook is provided', () => {
+ const customHooks: Hooks = {
+ onDragEnd: jest.fn(),
+ };
+
+ caller.onStateChange(customHooks, state.requesting(), state.dragging());
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart));
+ });
+
+ it('should announce with a provided message', () => {
+ const customHooks: Hooks = {
+ onDragStart: (start: DragStart, provided: HookProvided) => provided.announce('test'),
+ onDragEnd: jest.fn(),
+ };
+
+ caller.onStateChange(customHooks, state.requesting(), state.dragging());
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(announceMock).toHaveBeenCalledWith('test');
+ });
+
+ it('should prevent double announcing', () => {
+ let myAnnounce: ?Announce;
+ const customHooks: Hooks = {
+ onDragStart: (start: DragStart, provided: HookProvided) => {
+ myAnnounce = provided.announce;
+ myAnnounce('test');
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ caller.onStateChange(customHooks, state.requesting(), state.dragging());
+ expect(announceMock).toHaveBeenCalledWith('test');
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).not.toHaveBeenCalled();
+
+ if (!myAnnounce) {
+ throw new Error('Invalid test setup');
+ }
+
+ myAnnounce('second');
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should prevent async announcing', () => {
+ const customHooks: Hooks = {
+ onDragStart: (start: DragStart, provided: HookProvided) => {
+ setTimeout(() => {
+ // boom
+ provided.announce('too late');
+ });
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ caller.onStateChange(customHooks, state.requesting(), state.dragging());
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart));
+ expect(console.warn).not.toHaveBeenCalled();
+
+ // not releasing the async message
+ jest.runOnlyPendingTimers();
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('drag update', () => {
+ const withImpact = (current: State, impact: DragImpact) => {
+ if (!current.drag) {
+ throw new Error('invalid state');
+ }
+ return {
+ ...current,
+ drag: {
+ ...current.drag,
+ impact,
+ },
+ };
+ };
+
+ const start: DragStart = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+
+ const inHomeImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination: start.source,
+ };
+
+ describe('has not moved from home location', () => {
+ beforeEach(() => {
+ // start a drag
+ caller.onStateChange(
+ hooks,
+ state.requesting(),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+ });
+
+ it('should not provide an update if the location has not changed since the last drag', () => {
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+ });
+
+ it('should provide an update if the index changes', () => {
+ const destination: DraggableLocation = {
+ index: preset.inHome1.descriptor.index + 1,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const impact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), impact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should provide an update if the droppable changes', () => {
+ const destination: DraggableLocation = {
+ // same index
+ index: preset.inHome1.descriptor.index,
+ // different droppable
+ droppableId: preset.foreign.descriptor.id,
+ };
+ const impact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), impact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should provide an update if moving from a droppable to nothing', () => {
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: null,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), noImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ describe('announcements', () => {
+ const destination: DraggableLocation = {
+ // new index
+ index: preset.inHome1.descriptor.index + 1,
+ // different droppable
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const updateImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const dragUpdate: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+ const inHome = withImpact(state.dragging(), inHomeImpact);
+ const withUpdate = withImpact(state.dragging(), updateImpact);
+
+ const perform = (myHooks: Hooks) => {
+ caller.onStateChange(myHooks, inHome, withUpdate);
+ };
+
+ beforeEach(() => {
+ // from the lift
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ // clear its state
+ announceMock.mockReset();
+ });
+
+ it('should announce with the default update message if no message is provided', () => {
+ caller.onStateChange(hooks, inHome, withUpdate);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate));
+ });
+
+ it('should announce with the default update message if no onDragUpdate hook is provided', () => {
+ const customHooks: Hooks = {
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate));
+ });
+
+ it('should announce with a provided message', () => {
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'),
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(announceMock).toHaveBeenCalledWith('test');
+ });
+
+ it('should prevent double announcing', () => {
+ let myAnnounce: ?Announce;
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => {
+ myAnnounce = provided.announce;
+ myAnnounce('test');
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith('test');
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).not.toHaveBeenCalled();
+
+ if (!myAnnounce) {
+ throw new Error('Invalid test setup');
+ }
+
+ myAnnounce('second');
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should prevent async announcing', () => {
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => {
+ setTimeout(() => {
+ // boom
+ provided.announce('too late');
+ });
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate));
+ expect(console.warn).not.toHaveBeenCalled();
+
+ // not releasing the async message
+ jest.runOnlyPendingTimers();
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('no longer in home location', () => {
+ const firstImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ // moved into the second index
+ destination: {
+ index: preset.inHome1.descriptor.index + 1,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+
+ beforeEach(() => {
+ // initial lift
+ caller.onStateChange(
+ hooks,
+ state.requesting(),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ // checking everything is well
+ expect(hooks.onDragStart).toHaveBeenCalled();
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+
+ // first move into new location
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), firstImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalled();
+ // cleaning the hook
+ // $ExpectError - no mock reset property
+ hooks.onDragUpdate.mockReset();
+ });
+
+ it('should not provide an update if the location has not changed since the last drag', () => {
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ withImpact(state.dragging(), firstImpact),
+ );
+
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+ });
+
+ it('should provide an update if the index changes', () => {
+ const destination: DraggableLocation = {
+ index: preset.inHome1.descriptor.index + 2,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const secondImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ withImpact(state.dragging(), secondImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should provide an update if the droppable changes', () => {
+ const destination: DraggableLocation = {
+ index: preset.inHome1.descriptor.index + 1,
+ droppableId: preset.foreign.descriptor.id,
+ };
+ const secondImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ withImpact(state.dragging(), secondImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should provide an update if moving from a droppable to nothing', () => {
+ const secondImpact: DragImpact = {
+ movement: noMovement,
+ direction: null,
+ destination: null,
+ };
+ const expected: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: null,
+ };
+
+ // drag to the same spot
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ withImpact(state.dragging(), secondImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should provide an update if moving back to the home location', () => {
+ const impact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination: null,
+ };
+
+ // drag to nowhere
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), impact),
+ );
+ const first: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: null,
+ };
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(first, {
+ announce: expect.any(Function),
+ });
+
+ // drag back to home
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), impact),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ const second: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: start.source,
+ };
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, {
+ announce: expect.any(Function),
+ });
+ });
+
+ describe('announcements', () => {
+ const destination: DraggableLocation = {
+ // new index
+ index: preset.inHome1.descriptor.index + 2,
+ // different droppable
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const secondImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ destination,
+ };
+ const secondUpdate: DragUpdate = {
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination,
+ };
+ const withFirstUpdate = withImpact(state.dragging(), firstImpact);
+ const withSecondUpdate = withImpact(state.dragging(), secondImpact);
+
+ const perform = (myHooks: Hooks) => {
+ caller.onStateChange(myHooks, withFirstUpdate, withSecondUpdate);
+ };
+
+ beforeEach(() => {
+ // clear its state from previous updates
+ announceMock.mockReset();
+ });
+
+ it('should announce with the default update message if no message is provided', () => {
+ caller.onStateChange(hooks, withFirstUpdate, withSecondUpdate);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate));
+ });
+
+ it('should announce with the default update message if no onDragUpdate hook is provided', () => {
+ const customHooks: Hooks = {
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate));
+ });
+
+ it('should announce with a provided message', () => {
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'),
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(announceMock).toHaveBeenCalledWith('test');
+ });
+
+ it('should prevent double announcing', () => {
+ let myAnnounce: ?Announce;
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => {
+ myAnnounce = provided.announce;
+ myAnnounce('test');
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith('test');
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).not.toHaveBeenCalled();
+
+ if (!myAnnounce) {
+ throw new Error('Invalid test setup');
+ }
+
+ myAnnounce('second');
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should prevent async announcing', () => {
+ const customHooks: Hooks = {
+ onDragUpdate: (update: DragUpdate, provided: HookProvided) => {
+ setTimeout(() => {
+ // boom
+ provided.announce('too late');
+ });
+ },
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate));
+ expect(console.warn).not.toHaveBeenCalled();
+
+ // not releasing the async message
+ jest.runOnlyPendingTimers();
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('multiple updates', () => {
+ it('should correctly update across multiple updates', () => {
+ // initial lift
+ caller.onStateChange(
+ hooks,
+ state.requesting(),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ // checking everything is well
+ expect(hooks.onDragStart).toHaveBeenCalled();
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+
+ // first move into new location
+ const firstImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ // moved into the second index
+ destination: {
+ index: preset.inHome1.descriptor.index + 1,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), firstImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1);
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith({
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: firstImpact.destination,
+ }, { announce: expect.any(Function) });
+
+ // second move into new location
+ const secondImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ // moved into the second index
+ destination: {
+ index: preset.inHome1.descriptor.index + 2,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ withImpact(state.dragging(), secondImpact),
+ );
+
+ expect(hooks.onDragUpdate).toHaveBeenCalledTimes(2);
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith({
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: secondImpact.destination,
+ }, { announce: expect.any(Function) });
+ });
+
+ it('should update correctly across multiple drags', () => {
+ // initial lift
+ caller.onStateChange(
+ hooks,
+ state.requesting(),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ // checking everything is well
+ expect(hooks.onDragStart).toHaveBeenCalled();
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+
+ // first move into new location
+ const firstImpact: DragImpact = {
+ movement: noMovement,
+ direction: preset.home.axis.direction,
+ // moved into the second index
+ destination: {
+ index: preset.inHome1.descriptor.index + 1,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), firstImpact),
+ );
+ expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1);
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith({
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: firstImpact.destination,
+ }, { announce: expect.any(Function) });
+ // resetting the mock
+ // $ExpectError - resetting mock
+ hooks.onDragUpdate.mockReset();
+
+ // drop
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), firstImpact),
+ state.idle,
+ );
+
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+
+ // a new lift!
+ caller.onStateChange(
+ hooks,
+ state.requesting(),
+ withImpact(state.dragging(), inHomeImpact),
+ );
+ // checking everything is well
+ expect(hooks.onDragStart).toHaveBeenCalled();
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+
+ // first move into new location
+ caller.onStateChange(
+ hooks,
+ withImpact(state.dragging(), inHomeImpact),
+ withImpact(state.dragging(), firstImpact),
+ );
+ expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1);
+ expect(hooks.onDragUpdate).toHaveBeenCalledWith({
+ draggableId: start.draggableId,
+ type: start.type,
+ source: start.source,
+ destination: firstImpact.destination,
+ }, { announce: expect.any(Function) });
+ });
+ });
+ });
+
+ describe('drag end', () => {
+ // it is possible to complete a drag from a DRAGGING or DROP_ANIMATING (drop or cancel)
+ const preEndStates: State[] = [
+ state.dragging(),
+ state.dropAnimating(),
+ state.userCancel(),
+ ];
+
+ preEndStates.forEach((previous: State, index: number): void => {
+ describe(`end state ${index}`, () => {
+ it('should call onDragEnd with the drop result', () => {
+ const result: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index,
+ },
+ destination: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index + 1,
+ },
+ reason: 'DROP',
+ };
+ const current: State = {
+ phase: 'DROP_COMPLETE',
+ drop: {
+ pending: null,
+ result,
+ },
+ drag: null,
+ dimension: noDimensions,
+ };
+
+ caller.onStateChange(hooks, previous, current);
+
+ if (!current.drop || !current.drop.result) {
+ throw new Error('invalid state');
+ }
+
+ const provided: DropResult = current.drop.result;
+ expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should log an error and not call the callback if there is no drop result', () => {
+ const invalid: State = {
+ ...state.dropComplete(),
+ drop: null,
+ };
+
+ caller.onStateChange(hooks, previous, invalid);
+
+ expect(hooks.onDragEnd).not.toHaveBeenCalled();
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should call onDragEnd with null as the destination if there is no destination', () => {
+ const result: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index,
+ },
+ destination: null,
+ reason: 'DROP',
+ };
+ const current: State = {
+ phase: 'DROP_COMPLETE',
+ drop: {
+ pending: null,
+ result,
+ },
+ drag: null,
+ dimension: noDimensions,
+ };
+
+ caller.onStateChange(hooks, previous, current);
+
+ expect(hooks.onDragEnd).toHaveBeenCalledWith(result, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should call onDragEnd with original source if the item did not move', () => {
+ const source: DraggableLocation = {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index,
+ };
+ const result: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source,
+ destination: source,
+ reason: 'DROP',
+ };
+ const current: State = {
+ phase: 'DROP_COMPLETE',
+ drop: {
+ pending: null,
+ result,
+ },
+ drag: null,
+ dimension: noDimensions,
+ };
+ const expected: DropResult = {
+ draggableId: result.draggableId,
+ type: result.type,
+ source: result.source,
+ // destination has been cleared
+ destination: source,
+ reason: 'DROP',
+ };
+
+ caller.onStateChange(hooks, previous, current);
+
+ expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ describe('announcements', () => {
+ const result: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index,
+ },
+ destination: {
+ droppableId: preset.inHome1.descriptor.droppableId,
+ index: preset.inHome1.descriptor.index + 1,
+ },
+ reason: 'DROP',
+ };
+ const current: State = {
+ phase: 'DROP_COMPLETE',
+ drop: {
+ pending: null,
+ result,
+ },
+ drag: null,
+ dimension: noDimensions,
+ };
+
+ const perform = (myHooks: Hooks) => {
+ caller.onStateChange(myHooks, previous, current);
+ };
+
+ beforeEach(() => {
+ // clear its state from previous updates
+ announceMock.mockReset();
+ });
+
+ it('should announce with the default update message if no message is provided', () => {
+ perform(hooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result));
+ });
+
+ it('should announce with the default update message if no onDragEnd hook is provided', () => {
+ const customHooks: Hooks = {
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result));
+ });
+
+ it('should announce with a provided message', () => {
+ const customHooks: Hooks = {
+ onDragEnd: (drop: DropResult, provided: HookProvided) => provided.announce('the end'),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(announceMock).toHaveBeenCalledWith('the end');
+ });
+
+ it('should prevent double announcing', () => {
+ let myAnnounce: ?Announce;
+ const customHooks: Hooks = {
+ onDragEnd: (drop: DropResult, provided: HookProvided) => {
+ myAnnounce = provided.announce;
+ myAnnounce('test');
+ },
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith('test');
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).not.toHaveBeenCalled();
+
+ if (!myAnnounce) {
+ throw new Error('Invalid test setup');
+ }
+
+ myAnnounce('second');
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should prevent async announcing', () => {
+ const customHooks: Hooks = {
+ onDragEnd: (drop: DropResult, provided: HookProvided) => {
+ setTimeout(() => {
+ // boom
+ provided.announce('too late');
+ });
+ },
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result));
+ expect(console.warn).not.toHaveBeenCalled();
+
+ // not releasing the async message
+ jest.runOnlyPendingTimers();
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+
+ describe('drag cleared', () => {
+ describe('cleared while dragging', () => {
+ const drop: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ // $ExpectError - not checking for null
+ source: {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ destination: null,
+ reason: 'CANCEL',
+ };
+ it('should return a result with a null destination', () => {
+ caller.onStateChange(hooks, state.dragging(), state.idle);
+
+ expect(hooks.onDragEnd).toHaveBeenCalledWith(drop, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should log an error and do nothing if it cannot find a previous drag to publish', () => {
+ const invalid: State = {
+ phase: 'DRAGGING',
+ drag: null,
+ drop: null,
+ dimension: noDimensions,
+ };
+
+ caller.onStateChange(hooks, state.idle, invalid);
+
+ expect(hooks.onDragEnd).not.toHaveBeenCalled();
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ describe('announcements', () => {
+ const perform = (myHooks: Hooks) => {
+ caller.onStateChange(myHooks, state.dragging(), state.idle);
+ };
+
+ beforeEach(() => {
+ // clear its state from previous updates
+ announceMock.mockReset();
+ });
+
+ it('should announce with the default update message if no message is provided', () => {
+ perform(hooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop));
+ });
+
+ it('should announce with the default update message if no onDragEnd hook is provided', () => {
+ const customHooks: Hooks = {
+ onDragEnd: jest.fn(),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop));
+ });
+
+ it('should announce with a provided message', () => {
+ const customHooks: Hooks = {
+ onDragEnd: (dropResult: DropResult, provided: HookProvided) => provided.announce('the end'),
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(announceMock).toHaveBeenCalledWith('the end');
+ });
+
+ it('should prevent double announcing', () => {
+ let myAnnounce: ?Announce;
+ const customHooks: Hooks = {
+ onDragEnd: (dropResult: DropResult, provided: HookProvided) => {
+ myAnnounce = provided.announce;
+ myAnnounce('test');
+ },
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith('test');
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).not.toHaveBeenCalled();
+
+ if (!myAnnounce) {
+ throw new Error('Invalid test setup');
+ }
+
+ myAnnounce('second');
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+
+ it('should prevent async announcing', () => {
+ const customHooks: Hooks = {
+ onDragEnd: (dropResult: DropResult, provided: HookProvided) => {
+ setTimeout(() => {
+ // boom
+ provided.announce('too late');
+ });
+ },
+ };
+
+ perform(customHooks);
+
+ expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop));
+ expect(console.warn).not.toHaveBeenCalled();
+
+ // not releasing the async message
+ jest.runOnlyPendingTimers();
+
+ expect(announceMock).toHaveBeenCalledTimes(1);
+ expect(console.warn).toHaveBeenCalled();
+ });
+ });
+ });
+
+ // this should never really happen - but just being safe
+ describe('cleared while drop animating', () => {
+ it('should return a result with a null destination', () => {
+ const expected: DropResult = {
+ draggableId: preset.inHome1.descriptor.id,
+ type: preset.home.descriptor.type,
+ source: {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ destination: null,
+ reason: 'CANCEL',
+ };
+
+ caller.onStateChange(hooks, state.dropAnimating(), state.idle);
+
+ expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, {
+ announce: expect.any(Function),
+ });
+ });
+
+ it('should log an error and do nothing if it cannot find a previous drag to publish', () => {
+ const invalid: State = {
+ ...state.dropAnimating(),
+ drop: null,
+ };
+
+ caller.onStateChange(hooks, invalid, state.idle);
+
+ expect(hooks.onDragEnd).not.toHaveBeenCalled();
+ expect(console.error).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('phase unchanged', () => {
+ it('should not do anything if the previous and next phase are the same', () => {
+ Object.keys(state).forEach((key: string) => {
+ const current: State = state[key];
+
+ caller.onStateChange(hooks, current, current);
+
+ expect(hooks.onDragStart).not.toHaveBeenCalled();
+ expect(hooks.onDragUpdate).not.toHaveBeenCalled();
+ expect(hooks.onDragEnd).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
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
index fb12a2803e..b3e6e52a84 100644
--- 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
@@ -4,7 +4,7 @@ import { getDroppableDimension } from '../../../../src/state/dimension';
import getArea from '../../../../src/state/get-area';
import { add } from '../../../../src/state/position';
import { horizontal, vertical } from '../../../../src/state/axis';
-import getViewport from '../../../../src/state/visibility/get-viewport';
+import getViewport from '../../../../src/window/get-viewport';
import type {
Axis,
Position,
@@ -309,14 +309,20 @@ describe('get best cross axis droppable', () => {
// long droppable inside a shorter container - this should be clipped
bottom: 80,
}),
- frameClient: getArea({
- // not the same top value as source
- top: 20,
- // shares the left edge with the source
- left: 20,
- right: 40,
- bottom: 40,
- }),
+ closest: {
+ frameClient: getArea({
+ // not the same top value as source
+ top: 20,
+ // shares the left edge with the source
+ left: 20,
+ right: 40,
+ bottom: 40,
+ }),
+ scrollWidth: 20,
+ scrollHeight: 80,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const sibling2 = getDroppableDimension({
descriptor: {
@@ -635,13 +641,19 @@ describe('get best cross axis droppable', () => {
[axis.crossAxisStart]: 200,
[axis.crossAxisEnd]: 300,
}),
- frameClient: getArea({
- [axis.start]: 0,
- [axis.end]: 100,
- // frame hides subject
- [axis.crossAxisStart]: 400,
- [axis.crossAxisEnd]: 500,
- }),
+ closest: {
+ frameClient: getArea({
+ [axis.start]: 0,
+ [axis.end]: 100,
+ // frame hides subject
+ [axis.crossAxisStart]: 400,
+ [axis.crossAxisEnd]: 500,
+ }),
+ scroll: { x: 0, y: 0 },
+ scrollWidth: 100,
+ scrollHeight: 100,
+ shouldClipSubject: true,
+ },
});
const droppables: DroppableDimensionMap = {
[source.descriptor.id]: source,
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
index c2680f1f04..4b9b761a0c 100644
--- a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js
+++ b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js
@@ -1,11 +1,13 @@
// @flow
import getClosestDraggable from '../../../../src/state/move-cross-axis/get-closest-draggable';
-import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension';
+import { getDroppableDimension, getDraggableDimension, scrollDroppable } from '../../../../src/state/dimension';
import { add, distance, patch } from '../../../../src/state/position';
+import { expandByPosition } from '../../../../src/state/spacing';
import { horizontal, vertical } from '../../../../src/state/axis';
import getArea from '../../../../src/state/get-area';
-import getViewport from '../../../../src/state/visibility/get-viewport';
+import getViewport from '../../../../src/window/get-viewport';
import type {
+ Area,
Axis,
Position,
DraggableDimension,
@@ -14,308 +16,359 @@ import type {
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({
- descriptor: {
- id: 'droppable',
- type: 'TYPE',
- },
- client: getArea({
+ describe(`on the ${axis.direction} axis`, () => {
+ const start: number = 0;
+ const end: number = 100;
+ const crossAxisStart: number = 0;
+ const crossAxisEnd: number = 20;
+
+ const client: Area = getArea({
[axis.start]: start,
[axis.end]: end,
[axis.crossAxisStart]: crossAxisStart,
[axis.crossAxisEnd]: crossAxisEnd,
- }),
- });
-
- const hiddenBackwards: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'hiddenBackwards',
- droppableId: droppable.descriptor.id,
- index: 0,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: -30, // -10
- [axis.end]: -10,
- }),
- });
-
- // item bleeds backwards past the start of the droppable
- const partiallyHiddenBackwards: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'partialHiddenBackwards',
- droppableId: droppable.descriptor.id,
- index: 1,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: -10, // -10
- [axis.end]: 20,
- }),
- });
-
- const visible1: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'visible1',
- droppableId: droppable.descriptor.id,
- index: 2,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: 20,
- [axis.end]: 40,
- }),
- });
+ });
- const visible2: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'visible2',
- droppableId: droppable.descriptor.id,
- index: 3,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: 40,
- [axis.end]: 60,
- }),
- });
+ const droppable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'droppable',
+ type: 'TYPE',
+ },
+ direction: axis.direction,
+ client,
+ });
- // bleeds over the end of the visible boundary
- const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'partiallyHiddenForwards',
- droppableId: droppable.descriptor.id,
- index: 4,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: 60,
- [axis.end]: 120,
- }),
- });
+ const hiddenBackwards: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'hiddenBackwards',
+ droppableId: droppable.descriptor.id,
+ index: 0,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: -30, // -10
+ [axis.end]: -10,
+ }),
+ });
- // totally invisible
- const hiddenForwards: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'hiddenForwards',
- droppableId: droppable.descriptor.id,
- index: 5,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: 120,
- [axis.end]: 140,
- }),
- });
+ // item bleeds backwards past the start of the droppable
+ const partiallyHiddenBackwards: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'partialHiddenBackwards',
+ droppableId: droppable.descriptor.id,
+ index: 1,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: -10, // -10
+ [axis.end]: 20,
+ }),
+ });
- const viewport = getViewport();
- const outOfViewport: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'hidden',
- droppableId: droppable.descriptor.id,
- index: 6,
- },
- client: getArea({
- [axis.crossAxisStart]: crossAxisStart,
- [axis.crossAxisEnd]: crossAxisEnd,
- [axis.start]: viewport[axis.end] + 1,
- [axis.end]: viewport[axis.end] + 10,
- }),
- });
+ const visible1: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'visible1',
+ droppableId: droppable.descriptor.id,
+ index: 2,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 20,
+ [axis.end]: 40,
+ }),
+ });
- const insideDestination: DraggableDimension[] = [
- hiddenBackwards,
- partiallyHiddenBackwards,
- visible1,
- visible2,
- partiallyHiddenForwards,
- hiddenForwards,
- outOfViewport,
- ];
-
- 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,
+ const visible2: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'visible2',
+ droppableId: droppable.descriptor.id,
+ index: 3,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 40,
+ [axis.end]: 60,
+ }),
});
- 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,
+
+ // bleeds over the end of the visible boundary
+ const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'partiallyHiddenForwards',
+ droppableId: droppable.descriptor.id,
+ index: 4,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 60,
+ [axis.end]: 120,
+ }),
});
- 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: [],
+ // totally invisible
+ const hiddenForwards: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'hiddenForwards',
+ droppableId: droppable.descriptor.id,
+ index: 5,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 120,
+ [axis.end]: 140,
+ }),
});
- expect(result).toBe(null);
- });
+ const viewport = getViewport();
+ const outOfViewport: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'hidden',
+ droppableId: droppable.descriptor.id,
+ index: 6,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: viewport[axis.end] + 1,
+ [axis.end]: viewport[axis.end] + 10,
+ }),
+ });
- describe('removal of draggables that are visible', () => {
- it('should ignore draggables backward that have no visiblity', () => {
- const center: Position = patch(
- axis.line, hiddenBackwards.page.withoutMargin.center[axis.line], 100
+ const insideDestination: DraggableDimension[] = [
+ hiddenBackwards,
+ partiallyHiddenBackwards,
+ visible1,
+ visible2,
+ partiallyHiddenForwards,
+ hiddenForwards,
+ outOfViewport,
+ ];
+
+ it('should return the closest draggable', () => {
+ // closet to visible1
+ const center1: Position = patch(
+ axis.line, visible1.page.withoutMargin.center[axis.line], 100
);
-
- const result: ?DraggableDimension = getClosestDraggable({
+ const result1: ?DraggableDimension = getClosestDraggable({
axis,
- pageCenter: center,
+ pageCenter: center1,
destination: droppable,
insideDestination,
});
+ expect(result1).toBe(visible1);
- expect(result).toBe(partiallyHiddenBackwards);
- });
-
- it('should not ignore draggables that have backwards partial visiblility', () => {
- const center: Position = patch(
- axis.line, partiallyHiddenBackwards.page.withoutMargin.center[axis.line], 100
+ // closest to visible2
+ const center2: Position = patch(
+ axis.line, visible2.page.withoutMargin.center[axis.line], 100
);
-
- const result: ?DraggableDimension = getClosestDraggable({
+ const result2: ?DraggableDimension = getClosestDraggable({
axis,
- pageCenter: center,
+ pageCenter: center2,
destination: droppable,
insideDestination,
});
-
- expect(result).toBe(partiallyHiddenBackwards);
+ expect(result2).toBe(visible2);
});
- it('should not ignore draggables that have forward partial visiblility', () => {
- const center: Position = patch(
- axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100
- );
+ 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,
+ insideDestination: [],
});
- expect(result).toBe(partiallyHiddenForwards);
+ expect(result).toBe(null);
});
- it('should ignore draggables forward that have no visiblity', () => {
+ it('should take into account the change in droppable scroll', () => {
+ const scrollable: DroppableDimension = getDroppableDimension({
+ descriptor: droppable.descriptor,
+ direction: axis.direction,
+ client,
+ closest: {
+ frameClient: getArea(expandByPosition(client, patch(axis.line, 100))),
+ scrollHeight: client.width + 100,
+ scrollWidth: client.height + 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+ const scrolled: DroppableDimension = scrollDroppable(
+ scrollable,
+ patch(axis.line, 20)
+ );
const center: Position = patch(
- axis.line, hiddenForwards.page.withoutMargin.center[axis.line], 100
+ axis.line,
+ visible1.page.withoutMargin.center[axis.line],
+ 100
);
const result: ?DraggableDimension = getClosestDraggable({
axis,
pageCenter: center,
- destination: droppable,
+ destination: scrolled,
insideDestination,
});
- expect(result).toBe(partiallyHiddenForwards);
- });
-
- it('should ignore draggables that are outside of the viewport', () => {
- const center: Position = patch(
- axis.line, outOfViewport.page.withoutMargin.center[axis.line], 100
- );
+ expect(result).toBe(visible2);
- const result: ?DraggableDimension = getClosestDraggable({
+ // validation - with no scroll applied we are normally closer to visible1
+ const result1: ?DraggableDimension = getClosestDraggable({
axis,
pageCenter: center,
destination: droppable,
insideDestination,
});
+ expect(result1).toBe(visible1);
+ });
+
+ describe('removal of draggables that are visible', () => {
+ it('should ignore draggables backward that have no total visiblity', () => {
+ const center: Position = patch(
+ axis.line,
+ hiddenBackwards.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 backwards partial visiblility', () => {
+ const center: Position = patch(
+ axis.line,
+ partiallyHiddenBackwards.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 forward that have no visiblity', () => {
+ const center: Position = patch(
+ axis.line, hiddenForwards.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 are outside of the viewport', () => {
+ const center: Position = patch(
+ axis.line, outOfViewport.page.withoutMargin.center[axis.line], 100
+ );
+
+ const result: ?DraggableDimension = getClosestDraggable({
+ axis,
+ pageCenter: center,
+ destination: droppable,
+ insideDestination,
+ });
+
+ expect(result).toBe(visible2);
+ });
- expect(result).toBe(partiallyHiddenForwards);
+ it('should return null if there are no visible targets', () => {
+ const notVisible: DraggableDimension[] = [
+ hiddenBackwards,
+ hiddenForwards,
+ outOfViewport,
+ ];
+ 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 null if there are no visible targets', () => {
- const notVisible: DraggableDimension[] = [
- hiddenBackwards,
- hiddenForwards,
- outOfViewport,
- ];
- const center: Position = {
- x: 0,
- y: 0,
- };
+ 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: notVisible,
+ insideDestination,
});
- 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);
- expect(result).toBe(visible1);
+ // validating test assumptions
- // validating test assumptions
+ // 1. that they have equal distances
+ expect(distance(center, visible1.page.withoutMargin.center))
+ .toEqual(distance(center, visible2.page.withoutMargin.center));
- // 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,
+ // 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);
});
- 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
index 2aecfa77d9..72856c2722 100644
--- a/test/unit/state/move-cross-axis/move-cross-axis.spec.js
+++ b/test/unit/state/move-cross-axis/move-cross-axis.spec.js
@@ -1,7 +1,7 @@
// @flow
import moveCrossAxis from '../../../../src/state/move-cross-axis/';
import noImpact from '../../../../src/state/no-impact';
-import getViewport from '../../../../src/state/visibility/get-viewport';
+import getViewport from '../../../../src/window/get-viewport';
import getArea from '../../../../src/state/get-area';
import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension';
import { getPreset } from '../../../utils/dimension';
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
index 6995d0fa07..36af179307 100644
--- 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
@@ -1,14 +1,14 @@
// @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 { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
import getArea from '../../../../src/state/get-area';
import moveToEdge from '../../../../src/state/move-to-edge';
-import { patch } from '../../../../src/state/position';
+import { add, negate, patch } from '../../../../src/state/position';
import { horizontal, vertical } from '../../../../src/state/axis';
-import { getPreset } from '../../../utils/dimension';
+import { getPreset, makeScrollable } from '../../../utils/dimension';
import noImpact from '../../../../src/state/no-impact';
-import getViewport from '../../../../src/state/visibility/get-viewport';
+import getViewport from '../../../../src/window/get-viewport';
import type {
Axis,
DragImpact,
@@ -86,129 +86,217 @@ describe('move to new droppable', () => {
});
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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ // 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.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ 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 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: {
+ displaced: [],
+ amount: patch(axis.line, inHome2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.descriptor.id,
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should return an empty impact with the original location', () => {
- const expected: DragImpact = {
- movement: {
- displaced: [],
- amount: patch(axis.line, inHome2.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- destination: {
- droppableId: home.descriptor.id,
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(home, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome2,
+ target: inHome2,
+ destination: scrolled,
+ insideDestination: draggables,
+ home: {
index: 1,
+ droppableId: home.descriptor.id,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const expected: Position = add(inHome2.page.withoutMargin.center, displacement);
+
+ expect(result.pageCenter).toEqual(expected);
+ });
+
+ it('should return an empty impact with the original location', () => {
+ const expected: DragImpact = {
+ movement: {
+ displaced: [],
+ amount: patch(axis.line, inHome2.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.descriptor.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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ // 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.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ 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,
+ });
- 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);
});
- 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
+ displaced: [
+ {
+ draggableId: inHome2.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inHome3.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ ],
+ amount: patch(axis.line, inHome4.page.withMargin[axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.descriptor.id,
+ // original index of target
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should move the everything from the target index to the original index forward', () => {
- const expected: DragImpact = {
- movement: {
- // ordered by closest impacted
- displaced: [
- {
- draggableId: inHome2.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inHome3.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- ],
- amount: patch(axis.line, inHome4.page.withMargin[axis.size]),
- isBeyondStartPosition: false,
- },
- direction: axis.direction,
- destination: {
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(home, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome4,
+ target: inHome2,
+ destination: scrolled,
+ insideDestination: draggables,
+ home: {
+ index: 3,
droppableId: home.descriptor.id,
- // original index of target
- index: 1,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const withoutScroll: Position = moveToEdge({
+ source: inHome4.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inHome2.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: axis,
+ });
+ const expected: Position = add(withoutScroll, displacement);
+
+ expect(result.pageCenter).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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ // 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.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ 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,
@@ -220,42 +308,79 @@ describe('move to new droppable', () => {
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
+ displaced: [
+ {
+ draggableId: inHome4.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inHome3.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inHome2.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ ],
+ amount: patch(axis.line, inHome1.page.withMargin[axis.size]),
+ // is moving beyond start position
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ destination: {
+ droppableId: home.descriptor.id,
+ // original index of target
+ index: 3,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should move the everything from the target index to the original index forward', () => {
- const expected: DragImpact = {
- movement: {
- // ordered by closest impacted
- displaced: [
- {
- draggableId: inHome4.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inHome3.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inHome2.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- ],
- amount: patch(axis.line, inHome1.page.withMargin[axis.size]),
- // is moving beyond start position
- isBeyondStartPosition: true,
- },
- direction: axis.direction,
- destination: {
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(home, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: dontCare,
+ draggable: inHome1,
+ target: inHome4,
+ destination: scrolled,
+ insideDestination: draggables,
+ home: {
+ index: 0,
droppableId: home.descriptor.id,
- // original index of target
- index: 3,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const withoutScroll: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: inHome4.page.withoutMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+ const expected: Position = add(withoutScroll, displacement);
+
+ expect(result.pageCenter).toEqual(expected);
+ });
});
});
@@ -274,13 +399,19 @@ describe('move to new droppable', () => {
// will be cut by frame
[axis.end]: 200,
}),
- frameClient: getArea({
- [axis.crossAxisStart]: 0,
- [axis.crossAxisEnd]: 100,
- [axis.start]: 0,
- // will cut the subject
- [axis.end]: 100,
- }),
+ closest: {
+ frameClient: getArea({
+ [axis.crossAxisStart]: 0,
+ [axis.crossAxisEnd]: 100,
+ [axis.start]: 0,
+ // will cut the subject
+ [axis.end]: 100,
+ }),
+ scrollWidth: 200,
+ scrollHeight: 200,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const inside: DraggableDimension = getDraggableDimension({
descriptor: {
@@ -461,180 +592,298 @@ describe('move to new droppable', () => {
});
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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: null,
+ destination: foreign,
+ insideDestination: [],
+ home: {
+ index: 0,
+ droppableId: home.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ if (!result) {
+ throw new Error('invalid test setup');
+ }
+
+ it('should move to the start edge of the droppable (including its padding)', () => {
+ const expected: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign.page.withMarginAndPadding,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
- it('should move to the start edge of the droppable (including its padding)', () => {
- const expected: Position = moveToEdge({
- source: inHome1.page.withoutMargin,
- sourceEdge: 'start',
- destination: foreign.page.withMarginAndPadding,
- destinationEdge: 'start',
- destinationAxis: foreign.axis,
+ expect(result.pageCenter).toEqual(expected);
});
- expect(result.pageCenter).toEqual(expected);
+ it('should return an empty impact', () => {
+ const expected: DragImpact = {
+ movement: {
+ displaced: [],
+ amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.descriptor.id,
+ index: 0,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should return an empty impact', () => {
- const expected: DragImpact = {
- movement: {
- displaced: [],
- amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
- isBeyondStartPosition: false,
- },
- direction: foreign.axis.direction,
- destination: {
- droppableId: foreign.descriptor.id,
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(foreign, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: null,
+ destination: scrolled,
+ insideDestination: [],
+ home: {
index: 0,
+ droppableId: home.descriptor.id,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const withoutScroll: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: foreign.page.withMarginAndPadding,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
+ const expected: Position = add(withoutScroll, displacement);
+
+ expect(result.pageCenter).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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ // 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.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ 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,
+ });
- 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);
});
- expect(result.pageCenter).toEqual(expected);
+ it('should move the target and everything below it forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ displaced: [
+ {
+ draggableId: inForeign2.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inForeign3.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inForeign4.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ ],
+ amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.descriptor.id,
+ // index of foreign2
+ index: 1,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should move the target and everything below it forward', () => {
- const expected: DragImpact = {
- movement: {
- // ordered by closest impacted
- displaced: [
- {
- draggableId: inForeign2.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inForeign3.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inForeign4.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- ],
- amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]),
- isBeyondStartPosition: false,
- },
- direction: foreign.axis.direction,
- destination: {
- droppableId: foreign.descriptor.id,
- // index of foreign2
- index: 1,
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(foreign, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome1.page.withMargin.center,
+ draggable: inHome1,
+ target: inForeign2,
+ destination: scrolled,
+ insideDestination: draggables,
+ home: {
+ index: 0,
+ droppableId: home.descriptor.id,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const withoutScroll: Position = moveToEdge({
+ source: inHome1.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inForeign2.page.withMargin,
+ destinationEdge: 'start',
+ destinationAxis: foreign.axis,
+ });
+ const expected: Position = add(withoutScroll, displacement);
+
+ expect(result.pageCenter).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.descriptor.id,
- },
- previousImpact: noImpact,
- });
+ describe('without droppable scroll', () => {
+ // 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.descriptor.id,
+ },
+ previousImpact: noImpact,
+ });
- if (!result) {
- throw new Error('invalid test setup');
- }
+ 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,
+ 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);
});
- expect(result.pageCenter).toEqual(expected);
+ it('should move everything after the proposed index forward', () => {
+ const expected: DragImpact = {
+ movement: {
+ // ordered by closest impacted
+ displaced: [
+ {
+ draggableId: inForeign3.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inForeign4.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ ],
+ amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]),
+ isBeyondStartPosition: false,
+ },
+ direction: foreign.axis.direction,
+ destination: {
+ droppableId: foreign.descriptor.id,
+ // going after target, so index is target index + 1
+ index: 2,
+ },
+ };
+
+ expect(result.impact).toEqual(expected);
+ });
});
- it('should move everything after the proposed index forward', () => {
- const expected: DragImpact = {
- movement: {
- // ordered by closest impacted
- displaced: [
- {
- draggableId: inForeign3.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- {
- draggableId: inForeign4.descriptor.id,
- isVisible: true,
- shouldAnimate: true,
- },
- ],
- amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]),
- isBeyondStartPosition: false,
- },
- direction: foreign.axis.direction,
- destination: {
- droppableId: foreign.descriptor.id,
- // going after target, so index is target index + 1
- index: 2,
+ describe('with droppable scroll', () => {
+ const scrollable: DroppableDimension = makeScrollable(foreign, 10);
+ const scroll: Position = patch(axis.line, 10);
+ const displacement: Position = negate(scroll);
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10));
+
+ const result: ?Result = moveToNewDroppable({
+ pageCenter: inHome4.page.withMargin.center,
+ draggable: inHome4,
+ target: inForeign2,
+ destination: scrolled,
+ insideDestination: draggables,
+ home: {
+ index: 3,
+ droppableId: home.descriptor.id,
},
- };
+ previousImpact: noImpact,
+ });
- expect(result.impact).toEqual(expected);
+ if (!result) {
+ throw new Error('Invalid result');
+ }
+
+ it('should account for changes in droppable scroll', () => {
+ const withoutScroll: Position = moveToEdge({
+ source: inHome4.page.withoutMargin,
+ sourceEdge: 'start',
+ destination: inForeign2.page.withMargin,
+ // going after
+ destinationEdge: 'end',
+ destinationAxis: foreign.axis,
+ });
+ const expected: Position = add(withoutScroll, displacement);
+
+ expect(result.pageCenter).toEqual(expected);
+ });
});
});
@@ -679,12 +928,18 @@ describe('move to new droppable', () => {
// will be cut by frame
bottom: 200,
}),
- frameClient: getArea({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
- }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ scrollWidth: 200,
+ scrollHeight: 200,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const customInForeign: DraggableDimension = getDraggableDimension({
diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js
index 55d60232bd..45314e5b09 100644
--- a/test/unit/state/move-to-edge.spec.js
+++ b/test/unit/state/move-to-edge.spec.js
@@ -46,7 +46,7 @@ const destination: Area = getArea({
const pullBackwardsOnMainAxis = (axis: Axis) => (point: Position) => patch(
axis.line,
-point[axis.line],
- point[axis.crossLine]
+ point[axis.crossAxisLine]
);
// returns the absolute difference of the center position
diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js
index 7758012a3a..eb4a62bd2d 100644
--- a/test/unit/state/move-to-next-index.spec.js
+++ b/test/unit/state/move-to-next-index.spec.js
@@ -1,15 +1,15 @@
// @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 { getPreset, disableDroppable } from '../../utils/dimension';
+import { getPreset, disableDroppable, getClosestScrollable } from '../../utils/dimension';
import moveToEdge from '../../../src/state/move-to-edge';
import noImpact, { noMovement } from '../../../src/state/no-impact';
-import { patch } from '../../../src/state/position';
+import { patch, subtract } from '../../../src/state/position';
import { vertical, horizontal } from '../../../src/state/axis';
-import getViewport from '../../../src/state/visibility/get-viewport';
+import { isPartiallyVisible } from '../../../src/state/visibility/is-visible';
+import getViewport from '../../../src/window/get-viewport';
import getArea from '../../../src/state/get-area';
-import setWindowScroll from '../../utils/set-window-scroll';
-import { getDroppableDimension, getDraggableDimension } from '../../../src/state/dimension';
+import { getDroppableDimension, getDraggableDimension, scrollDroppable } from '../../../src/state/dimension';
import type {
Area,
Axis,
@@ -61,6 +61,7 @@ describe('move to next index', () => {
const result: ?Result = moveToNextIndex({
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
previousImpact: noImpact,
droppable: disabled,
draggables: preset.draggables,
@@ -87,6 +88,7 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome3.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome3.page.withoutMargin.center,
droppable: preset.home,
draggables: preset.draggables,
});
@@ -110,6 +112,7 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -167,6 +170,7 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome2.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome2.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -229,10 +233,14 @@ describe('move to next index', () => {
index: 1,
},
};
+
const result: ?Result = moveToNextIndex({
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct previous page center
+ // not calculating the exact point as it is not required for this test
+ previousPageCenter: preset.inHome2.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -311,6 +319,8 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome2.descriptor.id,
previousImpact,
+ // roughly correct:
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -384,6 +394,8 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome3.descriptor.id,
previousImpact,
+ // this is roughly correct
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -427,6 +439,274 @@ describe('move to next index', () => {
expect(result.impact).toEqual(expected);
});
});
+
+ describe('forced visibility displacement', () => {
+ const crossAxisStart: number = 0;
+ const crossAxisEnd: number = 100;
+
+ const droppableScrollSize = {
+ scrollHeight: axis === vertical ? 400 : crossAxisEnd,
+ scrollWidth: axis === horizontal ? 400 : crossAxisEnd,
+ };
+
+ const home: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'home',
+ type: 'TYPE',
+ },
+ direction: axis.direction,
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 0,
+ [axis.end]: 400,
+ }),
+ closest: {
+ frameClient: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 0,
+ // will cut off the subject
+ [axis.end]: 100,
+ }),
+ scrollHeight: droppableScrollSize.scrollHeight,
+ scrollWidth: droppableScrollSize.scrollWidth,
+ shouldClipSubject: true,
+ scroll: { x: 0, y: 0 },
+ },
+ });
+
+ const maxScroll: Position = getClosestScrollable(home).scroll.max;
+
+ // half the size of the viewport
+ const inHome1: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome1',
+ droppableId: home.descriptor.id,
+ index: 0,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 0,
+ [axis.end]: 50,
+ }),
+ });
+
+ const inHome2: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome2',
+ droppableId: home.descriptor.id,
+ index: 1,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 50,
+ [axis.end]: 100,
+ }),
+ });
+
+ const inHome3: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome3',
+ droppableId: home.descriptor.id,
+ index: 2,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 100,
+ [axis.end]: 150,
+ }),
+ });
+
+ const inHome4: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome4',
+ droppableId: home.descriptor.id,
+ index: 3,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 200,
+ [axis.end]: 250,
+ }),
+ });
+
+ const inHome5: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome5',
+ droppableId: home.descriptor.id,
+ index: 4,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 300,
+ [axis.end]: 350,
+ }),
+ });
+
+ const inHome6: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inHome5',
+ droppableId: home.descriptor.id,
+ index: 5,
+ },
+ client: getArea({
+ [axis.crossAxisStart]: crossAxisStart,
+ [axis.crossAxisEnd]: crossAxisEnd,
+ [axis.start]: 350,
+ [axis.end]: 400,
+ }),
+ });
+
+ const draggables: DraggableDimensionMap = {
+ [inHome1.descriptor.id]: inHome1,
+ [inHome2.descriptor.id]: inHome2,
+ [inHome3.descriptor.id]: inHome3,
+ [inHome4.descriptor.id]: inHome4,
+ [inHome5.descriptor.id]: inHome5,
+ [inHome6.descriptor.id]: inHome6,
+ };
+
+ it('should force the displacement of the items up to the size of the item dragging and the item no longer being displaced', () => {
+ // We have moved inHome1 to the end of the list
+ const previousImpact: DragImpact = {
+ movement: {
+ // ordered by most recently impacted
+ displaced: [
+ // the last impact would have been before the last addition.
+ // At this point the last two items would have been visible
+ {
+ draggableId: inHome6.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inHome5.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ {
+ draggableId: inHome4.descriptor.id,
+ isVisible: false,
+ shouldAnimate: false,
+ },
+ {
+ draggableId: inHome3.descriptor.id,
+ isVisible: false,
+ shouldAnimate: false,
+ },
+ {
+ draggableId: inHome2.descriptor.id,
+ isVisible: false,
+ shouldAnimate: false,
+ },
+ ],
+ amount: patch(axis.line, inHome1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // is now in the last position
+ destination: {
+ droppableId: home.descriptor.id,
+ index: 4,
+ },
+ };
+ // home has now scrolled to the bottom
+ const scrolled: DroppableDimension = scrollDroppable(home, maxScroll);
+
+ // validation of previous impact
+ expect(isPartiallyVisible({
+ target: inHome6.page.withMargin,
+ destination: scrolled,
+ viewport: customViewport,
+ })).toBe(true);
+ expect(isPartiallyVisible({
+ target: inHome5.page.withMargin,
+ destination: scrolled,
+ viewport: customViewport,
+ })).toBe(true);
+ expect(isPartiallyVisible({
+ target: inHome4.page.withMargin,
+ destination: scrolled,
+ viewport: customViewport,
+ })).toBe(false);
+ expect(isPartiallyVisible({
+ target: inHome3.page.withMargin,
+ destination: scrolled,
+ viewport: customViewport,
+ })).toBe(false);
+ // this one will remain invisible
+ expect(isPartiallyVisible({
+ target: inHome2.page.withMargin,
+ destination: scrolled,
+ viewport: customViewport,
+ })).toBe(false);
+
+ const expected: DragImpact = {
+ movement: {
+ // ordered by most recently impacted
+ displaced: [
+ // shouldAnimate has not changed to false - using previous impact
+ {
+ draggableId: inHome5.descriptor.id,
+ isVisible: true,
+ shouldAnimate: true,
+ },
+ // was not visibile - now forcing to be visible
+ // (within the size of the dragging item (50px) and the moving item (50px))
+ {
+ draggableId: inHome4.descriptor.id,
+ isVisible: true,
+ shouldAnimate: false,
+ },
+ // was not visibile - now forcing to be visible
+ // (within the size of the dragging item (50px) and the moving item (50px))
+ {
+ draggableId: inHome3.descriptor.id,
+ isVisible: true,
+ shouldAnimate: false,
+ },
+ // still not visible
+ // not within the 100px buffer
+ {
+ draggableId: inHome2.descriptor.id,
+ isVisible: false,
+ shouldAnimate: false,
+ },
+ ],
+ amount: patch(axis.line, inHome1.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ direction: axis.direction,
+ // is now in the second last position
+ destination: {
+ droppableId: home.descriptor.id,
+ index: 3,
+ },
+ };
+
+ const result: ?Result = moveToNextIndex({
+ isMovingForward: false,
+ draggableId: inHome1.descriptor.id,
+ previousImpact,
+ // roughly correct:
+ previousPageCenter: inHome1.page.withoutMargin.center,
+ draggables,
+ droppable: scrolled,
+ });
+
+ if (!result) {
+ throw new Error('Invalid test setup');
+ }
+
+ expect(result.impact).toEqual(expected);
+ });
+ });
});
});
@@ -449,6 +729,7 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -475,6 +756,7 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome2.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome2.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -535,6 +817,7 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome3.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome3.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -603,6 +886,8 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome2.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inHome3.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -676,6 +961,8 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inHome3.page.withoutMargin.center,
draggables: preset.draggables,
droppable: preset.home,
});
@@ -728,80 +1015,149 @@ describe('move to next index', () => {
id: 'much bigger than viewport',
type: 'huge',
},
+ direction: axis.direction,
client: getArea({
top: 0,
right: 10000,
bottom: 10000,
left: 0,
}),
- direction: axis.direction,
- });
- const inViewport: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'inside',
- index: 0,
- droppableId: droppable.descriptor.id,
- },
- client: customViewport,
});
- const outsideViewport: DraggableDimension = getDraggableDimension({
- descriptor: {
- id: 'outside',
- index: 1,
- droppableId: droppable.descriptor.id,
- },
- client: getArea({
- // is bottom left of the viewport
- top: customViewport.bottom + 1,
- right: customViewport.right + 100,
- left: customViewport.right + 1,
- bottom: customViewport.bottom + 100,
- }),
- });
- // inViewport is in its original position
- const previousImpact: DragImpact = {
- movement: noMovement,
- direction: axis.direction,
- destination: {
- index: 0,
- droppableId: droppable.descriptor.id,
- },
- };
- const draggables: DraggableDimensionMap = {
- [inViewport.descriptor.id]: inViewport,
- [outsideViewport.descriptor.id]: outsideViewport,
- };
- it('should not permit movement into areas that are outside the viewport', () => {
+ it('should request a jump scroll for movement that is outside of the viewport', () => {
+ const asBigAsViewport: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inside',
+ index: 0,
+ droppableId: droppable.descriptor.id,
+ },
+ client: customViewport,
+ });
+ const outsideViewport: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'outside',
+ index: 1,
+ droppableId: droppable.descriptor.id,
+ },
+ client: getArea({
+ // is bottom left of the viewport
+ top: customViewport.bottom + 1,
+ right: customViewport.right + 100,
+ left: customViewport.right + 1,
+ bottom: customViewport.bottom + 100,
+ }),
+ });
+ // inViewport is in its original position
+ const previousImpact: DragImpact = {
+ movement: noMovement,
+ direction: axis.direction,
+ destination: {
+ index: 0,
+ droppableId: droppable.descriptor.id,
+ },
+ };
+ const draggables: DraggableDimensionMap = {
+ [asBigAsViewport.descriptor.id]: asBigAsViewport,
+ [outsideViewport.descriptor.id]: outsideViewport,
+ };
+ const expectedCenter = moveToEdge({
+ source: asBigAsViewport.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: outsideViewport.page.withMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+ const previousPageCenter: Position = asBigAsViewport.page.withoutMargin.center;
+ const expectedScrollJump: Position = subtract(expectedCenter, previousPageCenter);
+ const expectedImpact: DragImpact = {
+ movement: {
+ displaced: [{
+ draggableId: outsideViewport.descriptor.id,
+ // Even though the item started in an invisible place we force
+ // the displacement to be visible.
+ isVisible: true,
+ shouldAnimate: true,
+ }],
+ amount: patch(axis.line, asBigAsViewport.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ destination: {
+ droppableId: droppable.descriptor.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
+
const result: ?Result = moveToNextIndex({
isMovingForward: true,
- draggableId: inViewport.descriptor.id,
+ draggableId: asBigAsViewport.descriptor.id,
previousImpact,
+ previousPageCenter,
draggables,
droppable,
});
- expect(result).toBe(null);
+ if (!result) {
+ throw new Error('Invalid test setup');
+ }
+
+ // not updating the page center (visually the item will not move)
+ expect(result.pageCenter).toEqual(previousPageCenter);
+ expect(result.scrollJumpRequest).toEqual(expectedScrollJump);
+ expect(result.impact).toEqual(expectedImpact);
});
- it('should take into account any changes in the droppables scroll', () => {
- // scrolling so that outsideViewport is now visible
- setWindowScroll({ x: 200, y: 200 });
- const expectedCenter = moveToEdge({
- source: inViewport.page.withoutMargin,
- sourceEdge: 'end',
- destination: outsideViewport.page.withMargin,
- destinationEdge: 'end',
- destinationAxis: axis,
+ it('should force visible displacement when displacing an invisible item', () => {
+ const visible: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'inside',
+ index: 0,
+ droppableId: droppable.descriptor.id,
+ },
+ client: getArea({
+ top: 0,
+ left: 0,
+ right: customViewport.right - 100,
+ bottom: customViewport.bottom - 100,
+ }),
});
+ const invisible: DraggableDimension = getDraggableDimension({
+ descriptor: {
+ id: 'partial',
+ index: 1,
+ droppableId: droppable.descriptor.id,
+ },
+ client: getArea({
+ top: customViewport.bottom + 1,
+ left: customViewport.right + 1,
+ bottom: customViewport.bottom + 100,
+ right: customViewport.right + 100,
+ }),
+ });
+ // inViewport is in its original position
+ const previousImpact: DragImpact = {
+ movement: noMovement,
+ direction: axis.direction,
+ destination: {
+ index: 0,
+ droppableId: droppable.descriptor.id,
+ },
+ };
+ const draggables: DraggableDimensionMap = {
+ [visible.descriptor.id]: visible,
+ [invisible.descriptor.id]: invisible,
+ };
+ const previousPageCenter: Position = visible.page.withoutMargin.center;
const expectedImpact: DragImpact = {
movement: {
displaced: [{
- draggableId: outsideViewport.descriptor.id,
+ draggableId: invisible.descriptor.id,
+ // Even though the item started in an invisible place we force
+ // the displacement to be visible.
isVisible: true,
shouldAnimate: true,
}],
- amount: patch(axis.line, inViewport.page.withMargin[axis.size]),
+ amount: patch(axis.line, visible.page.withMargin[axis.size]),
isBeyondStartPosition: true,
},
destination: {
@@ -813,28 +1169,29 @@ describe('move to next index', () => {
const result: ?Result = moveToNextIndex({
isMovingForward: true,
- draggableId: inViewport.descriptor.id,
+ draggableId: visible.descriptor.id,
previousImpact,
+ previousPageCenter,
draggables,
droppable,
});
if (!result) {
- throw new Error('invalid result');
+ throw new Error('Invalid test setup');
}
- expect(result.pageCenter).toEqual(expectedCenter);
expect(result.impact).toEqual(expectedImpact);
});
});
describe('droppable visibility', () => {
- it('should not permit movement into areas that outside of the droppable frame', () => {
+ it('should request a scroll jump into non-visible areas', () => {
const droppable: DroppableDimension = getDroppableDimension({
descriptor: {
id: 'much bigger than viewport',
type: 'huge',
},
+ direction: axis.direction,
client: getArea({
top: 0,
left: 0,
@@ -842,13 +1199,18 @@ describe('move to next index', () => {
bottom: 200,
right: 200,
}),
- frameClient: getArea({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
- }),
- direction: axis.direction,
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ }),
+ scrollHeight: 200,
+ scrollWidth: 200,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const inside: DraggableDimension = getDraggableDimension({
descriptor: {
@@ -878,7 +1240,6 @@ describe('move to next index', () => {
bottom: 180,
}),
});
- // inViewport is in its original position
const previousImpact: DragImpact = {
movement: noMovement,
direction: axis.direction,
@@ -891,20 +1252,50 @@ describe('move to next index', () => {
[inside.descriptor.id]: inside,
[outside.descriptor.id]: outside,
};
+ const previousPageCenter: Position = inside.page.withoutMargin.center;
+ const expectedCenter = moveToEdge({
+ source: inside.page.withoutMargin,
+ sourceEdge: 'end',
+ destination: outside.page.withMargin,
+ destinationEdge: 'end',
+ destinationAxis: axis,
+ });
+ const expectedScrollJump: Position = subtract(expectedCenter, previousPageCenter);
+ const expectedImpact: DragImpact = {
+ movement: {
+ displaced: [{
+ draggableId: outside.descriptor.id,
+ // Even though the item started in an invisible place we force
+ // the displacement to be visible.
+ isVisible: true,
+ shouldAnimate: true,
+ }],
+ amount: patch(axis.line, inside.page.withMargin[axis.size]),
+ isBeyondStartPosition: true,
+ },
+ destination: {
+ droppableId: droppable.descriptor.id,
+ index: 1,
+ },
+ direction: axis.direction,
+ };
const result: ?Result = moveToNextIndex({
isMovingForward: true,
draggableId: inside.descriptor.id,
previousImpact,
+ previousPageCenter,
draggables,
droppable,
});
- expect(result).toBe(null);
- });
+ if (!result) {
+ throw new Error('Invalid test setup');
+ }
- it.skip('should take into account any changes in the droppables scroll', () => {
- // TODO
+ expect(result.pageCenter).toEqual(previousPageCenter);
+ expect(result.impact).toEqual(expectedImpact);
+ expect(result.scrollJumpRequest).toEqual(expectedScrollJump);
});
});
});
@@ -951,6 +1342,7 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
@@ -1026,6 +1418,7 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ previousPageCenter: preset.inHome1.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
@@ -1085,6 +1478,8 @@ describe('move to next index', () => {
isMovingForward: true,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inHome4.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
@@ -1138,6 +1533,8 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inForeign1.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
@@ -1169,6 +1566,8 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inForeign4.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
@@ -1256,6 +1655,8 @@ describe('move to next index', () => {
isMovingForward: false,
draggableId: preset.inHome1.descriptor.id,
previousImpact,
+ // roughly correct
+ previousPageCenter: preset.inForeign2.page.withoutMargin.center,
droppable: preset.foreign,
draggables: preset.draggables,
});
diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js
index 6d97b55aca..cd89a8a282 100644
--- a/test/unit/state/position.spec.js
+++ b/test/unit/state/position.spec.js
@@ -1,6 +1,7 @@
// @flow
import {
add,
+ apply,
subtract,
isEqual,
negate,
@@ -50,6 +51,10 @@ describe('position', () => {
it('should return false when two objects have different values', () => {
expect(isEqual(point1, point2)).toBe(false);
});
+
+ it('should return true when -origin is compared with +origin', () => {
+ expect(isEqual({ x: -0, y: -0 }, { x: 0, y: 0 })).toBe(true);
+ });
});
describe('negate', () => {
@@ -125,4 +130,12 @@ describe('position', () => {
expect(closest(origin, [option1, option2])).toEqual(distance(origin, option1));
});
});
+
+ describe('apply', () => {
+ it('should apply the function to both values', () => {
+ const add1 = apply((value: number) => value + 1);
+
+ expect(add1({ x: 1, y: 2 })).toEqual({ x: 2, y: 3 });
+ });
+ });
});
diff --git a/test/unit/state/spacing.spec.js b/test/unit/state/spacing.spec.js
index 5855660ab0..c19103a92d 100644
--- a/test/unit/state/spacing.spec.js
+++ b/test/unit/state/spacing.spec.js
@@ -1,10 +1,10 @@
// @flow
import {
- add,
- addPosition,
isEqual,
- offset,
getCorners,
+ expandByPosition,
+ offsetByPosition,
+ expandBySpacing,
} from '../../../src/state/spacing';
import type { Position, Spacing } from '../../../src/types';
@@ -23,21 +23,9 @@ const spacing2: Spacing = {
};
describe('spacing', () => {
- describe('add', () => {
- it('should add two spacing boxes together', () => {
- const expected: Spacing = {
- top: 11,
- right: 26,
- bottom: 37,
- left: 14,
- };
- expect(add(spacing1, spacing2)).toEqual(expected);
- });
- });
-
- describe('addPosition', () => {
- it('should add a position to the right and bottom bounds of a spacing box', () => {
- const spacing = {
+ describe('expandByPosition', () => {
+ it('should increase the size of the spacing', () => {
+ const spacing: Spacing = {
top: 0,
right: 10,
bottom: 10,
@@ -48,12 +36,32 @@ describe('spacing', () => {
y: 5,
};
const expected = {
- top: 0,
+ top: -5,
right: 15,
bottom: 15,
+ left: -5,
+ };
+
+ expect(expandByPosition(spacing, position)).toEqual(expected);
+ });
+ });
+
+ describe('expandBySpacing', () => {
+ it('should increase the size of a spacing by the size of another', () => {
+ const spacing: Spacing = {
+ top: 10,
+ right: 20,
+ bottom: 20,
+ left: 10,
+ };
+ const expected: Spacing = {
+ top: 0,
+ right: 40,
+ bottom: 40,
left: 0,
};
- expect(addPosition(spacing, position)).toEqual(expected);
+
+ expect(expandBySpacing(spacing, spacing)).toEqual(expected);
});
});
@@ -72,7 +80,7 @@ describe('spacing', () => {
});
});
- describe('offset', () => {
+ describe('offsetByPosition', () => {
it('should add x/y values to top/right/bottom/left dimensions', () => {
const offsetPosition: Position = {
x: 10,
@@ -84,7 +92,7 @@ describe('spacing', () => {
bottom: 28,
left: 15,
};
- expect(offset(spacing1, offsetPosition)).toEqual(expected);
+ expect(offsetByPosition(spacing1, offsetPosition)).toEqual(expected);
});
});
diff --git a/test/unit/state/visibility/is-visible-through-frame.spec.js b/test/unit/state/visibility/is-partially-visible-through-frame.spec.js
similarity index 60%
rename from test/unit/state/visibility/is-visible-through-frame.spec.js
rename to test/unit/state/visibility/is-partially-visible-through-frame.spec.js
index 7e0d3ccdec..92199ee713 100644
--- a/test/unit/state/visibility/is-visible-through-frame.spec.js
+++ b/test/unit/state/visibility/is-partially-visible-through-frame.spec.js
@@ -1,29 +1,29 @@
// @flow
-import isVisibleThroughFrame from '../../../../src/state/visibility/is-visible-through-frame';
-import { add, offset } from '../../../../src/state/spacing';
+import isPartiallyVisibleThroughFrame from '../../../../src/state/visibility/is-partially-visible-through-frame';
+import { offsetByPosition, expandBySpacing } from '../../../../src/state/spacing';
import type { Spacing } from '../../../../src/types';
const frame: Spacing = {
top: 0, left: 0, right: 100, bottom: 100,
};
-describe('is visible through frame', () => {
+describe('is partially visible through frame', () => {
describe('subject is smaller than frame', () => {
describe('completely outside frame', () => {
it('should return false if subject is outside frame on any side', () => {
const outside: Spacing[] = [
// outside on top
- offset(frame, { x: 0, y: -101 }),
+ offsetByPosition(frame, { x: 0, y: -101 }),
// outside on right
- offset(frame, { x: 101, y: 0 }),
+ offsetByPosition(frame, { x: 101, y: 0 }),
// outside on bottom
- offset(frame, { x: 0, y: 101 }),
+ offsetByPosition(frame, { x: 0, y: 101 }),
// outside on left
- offset(frame, { x: -101, y: 0 }),
+ offsetByPosition(frame, { x: -101, y: 0 }),
];
outside.forEach((subject: Spacing) => {
- expect(isVisibleThroughFrame(frame)(subject)).toBe(false);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(false);
});
});
});
@@ -37,7 +37,7 @@ describe('is visible through frame', () => {
bottom: 90,
};
- expect(isVisibleThroughFrame(frame)(subject)).toBe(true);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true);
});
});
@@ -54,55 +54,29 @@ describe('is visible through frame', () => {
right: 150,
};
- expect(isVisibleThroughFrame(frame)(subject)).toBe(true);
- });
-
- it('should return false when only partially visible horizontally', () => {
- const subject: Spacing = {
- // visible
- left: 50,
- // not visible
- top: 110,
- bottom: 150,
- right: 150,
- };
-
- expect(isVisibleThroughFrame(frame)(subject)).toBe(false);
- });
-
- it('should return false when only partially visible vertically', () => {
- const subject: Spacing = {
- // visible
- top: 10,
- // not visible
- bottom: 110,
- left: 110,
- right: 150,
- };
-
- expect(isVisibleThroughFrame(frame)(subject)).toBe(false);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true);
});
});
});
describe('subject is equal to frame', () => {
it('should return true when the frame is equal to the subject', () => {
- expect(isVisibleThroughFrame(frame)(frame)).toBe(true);
- expect(isVisibleThroughFrame(frame)({ ...frame })).toBe(true);
+ expect(isPartiallyVisibleThroughFrame(frame)(frame)).toBe(true);
+ expect(isPartiallyVisibleThroughFrame(frame)({ ...frame })).toBe(true);
});
});
describe('subject is bigger than frame', () => {
- const bigSubject: Spacing = add(frame, frame);
+ const bigSubject: Spacing = expandBySpacing(frame, frame);
it('should return false if the subject has no overlap with the frame', () => {
- const subject: Spacing = offset(bigSubject, { x: 1000, y: 1000 });
+ const subject: Spacing = offsetByPosition(bigSubject, { x: 1000, y: 1000 });
- expect(isVisibleThroughFrame(frame)(subject)).toBe(false);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(false);
});
it('should return true if subject is bigger on every side', () => {
- expect(isVisibleThroughFrame(frame)(bigSubject)).toBe(true);
+ expect(isPartiallyVisibleThroughFrame(frame)(bigSubject)).toBe(true);
});
describe('partially visible', () => {
@@ -120,7 +94,7 @@ describe('is visible through frame', () => {
];
subjects.forEach((subject: Spacing) => {
- expect(isVisibleThroughFrame(frame)(subject)).toBe(true);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true);
});
});
@@ -134,7 +108,7 @@ describe('is visible through frame', () => {
right: 500,
};
- expect(isVisibleThroughFrame(frame)(subject)).toEqual(false);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toEqual(false);
});
it('should return false when only partially visible vertically', () => {
@@ -147,7 +121,7 @@ describe('is visible through frame', () => {
right: 500,
};
- expect(isVisibleThroughFrame(frame)(subject)).toEqual(false);
+ expect(isPartiallyVisibleThroughFrame(frame)(subject)).toEqual(false);
});
});
});
diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js
index e69e6b46b4..daad3296aa 100644
--- a/test/unit/state/visibility/is-partially-visible.spec.js
+++ b/test/unit/state/visibility/is-partially-visible.spec.js
@@ -1,8 +1,9 @@
// @flow
import getArea from '../../../../src/state/get-area';
-import isPartiallyVisible from '../../../../src/state/visibility/is-partially-visible';
+import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible';
import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
-import { offset } from '../../../../src/state/spacing';
+import { offsetByPosition } from '../../../../src/state/spacing';
+import { getClosestScrollable } from '../../../utils/dimension';
import type {
Area,
DroppableDimension,
@@ -45,7 +46,7 @@ const notInViewport: Spacing = {
bottom: 600,
};
-const smallDroppable: DroppableDimension = getDroppableDimension({
+const asBigAsInViewport1: DroppableDimension = getDroppableDimension({
descriptor: {
id: 'subset',
type: 'TYPE',
@@ -83,13 +84,13 @@ describe('is partially visible', () => {
it('should return true if the item is partially visible in the viewport', () => {
const partials: Spacing[] = [
// bleed over top
- offset(viewport, { x: 0, y: -1 }),
+ offsetByPosition(viewport, { x: 0, y: -1 }),
// bleed over right
- offset(viewport, { x: 1, y: 0 }),
+ offsetByPosition(viewport, { x: 1, y: 0 }),
// bleed over bottom
- offset(viewport, { x: 0, y: 1 }),
+ offsetByPosition(viewport, { x: 0, y: 1 }),
// bleed over left
- offset(viewport, { x: -1, y: 0 }),
+ offsetByPosition(viewport, { x: -1, y: 0 }),
];
partials.forEach((partial: Spacing) => {
@@ -115,6 +116,13 @@ describe('is partially visible', () => {
left: viewport.left,
right: viewport.right,
}),
+ closest: {
+ frameClient: viewport,
+ scrollWidth: viewport.width,
+ scrollHeight: viewport.bottom + 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
describe('originally invisible but now invisible', () => {
@@ -170,12 +178,40 @@ describe('is partially visible', () => {
});
describe('droppable', () => {
+ const client: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 600,
+ bottom: 600,
+ });
+ const frame: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ });
+
+ const scrollable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'clipped',
+ type: 'TYPE',
+ },
+ client,
+ closest: {
+ frameClient: frame,
+ scrollHeight: client.height,
+ scrollWidth: client.width,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
describe('without changes in droppable scroll', () => {
it('should return false if outside the droppable', () => {
expect(isPartiallyVisible({
target: inViewport2,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(false);
});
@@ -183,7 +219,7 @@ describe('is partially visible', () => {
expect(isPartiallyVisible({
target: viewport,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(true);
});
@@ -191,7 +227,7 @@ describe('is partially visible', () => {
expect(isPartiallyVisible({
target: inViewport1,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(true);
});
@@ -206,33 +242,33 @@ describe('is partially visible', () => {
expect(isPartiallyVisible({
target: insideDroppable,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(true);
});
it('should return true if partially within the droppable', () => {
const partials: Spacing[] = [
// bleed over top
- offset(inViewport1, { x: 0, y: -1 }),
+ offsetByPosition(inViewport1, { x: 0, y: -1 }),
// bleed over right
- offset(inViewport1, { x: 1, y: 0 }),
+ offsetByPosition(inViewport1, { x: 1, y: 0 }),
// bleed over bottom
- offset(inViewport1, { x: 0, y: 1 }),
+ offsetByPosition(inViewport1, { x: 0, y: 1 }),
// bleed over left
- offset(inViewport1, { x: -1, y: 0 }),
+ offsetByPosition(inViewport1, { x: -1, y: 0 }),
];
partials.forEach((partial: Spacing) => {
expect(isPartiallyVisible({
target: partial,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(true);
});
});
it('should return false if falling on clipped area of droppable', () => {
- const frame: Spacing = {
+ const ourFrame: Spacing = {
top: 10,
left: 10,
right: 100,
@@ -245,11 +281,17 @@ describe('is partially visible', () => {
type: 'TYPE',
},
client: getArea({
- ...frame,
+ ...ourFrame,
// stretches out past frame
bottom: 600,
}),
- frameClient: getArea(frame),
+ closest: {
+ frameClient: getArea(ourFrame),
+ scrollHeight: 600,
+ scrollWidth: getArea(ourFrame).width,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
});
const inSubjectOutsideFrame: Spacing = {
...frame,
@@ -266,45 +308,26 @@ describe('is partially visible', () => {
});
describe('with changes in droppable scroll', () => {
- const frame: Spacing = {
- top: 10,
- left: 10,
- right: 100,
- // cuts the droppable short
- bottom: 100,
- };
- const clippedDroppable: DroppableDimension = getDroppableDimension({
- descriptor: {
- id: 'clipped',
- type: 'TYPE',
- },
- client: getArea({
- ...frame,
- // stretches out past frame
- bottom: 600,
- }),
- frameClient: getArea(frame),
- });
-
describe('originally invisible but now invisible', () => {
it('should take into account the droppable scroll when detecting visibility', () => {
const originallyInvisible: Spacing = {
- ...frame,
- top: 110,
- bottom: 200,
+ left: frame.left,
+ right: frame.right,
+ top: frame.bottom + 10,
+ bottom: frame.bottom + 20,
};
// originally invisible
expect(isPartiallyVisible({
target: originallyInvisible,
- destination: clippedDroppable,
+ destination: scrollable,
viewport,
})).toBe(false);
// after scroll the target is now visible
expect(isPartiallyVisible({
target: originallyInvisible,
- destination: scrollDroppable(clippedDroppable, { x: 0, y: 100 }),
+ destination: scrollDroppable(scrollable, { x: 0, y: 100 }),
viewport,
})).toBe(true);
});
@@ -321,14 +344,14 @@ describe('is partially visible', () => {
// originally visible
expect(isPartiallyVisible({
target: originallyVisible,
- destination: clippedDroppable,
+ destination: scrollable,
viewport,
})).toBe(true);
// after scroll the target is now invisible
expect(isPartiallyVisible({
target: originallyVisible,
- destination: scrollDroppable(clippedDroppable, { x: 0, y: 100 }),
+ destination: scrollDroppable(scrollable, { x: 0, y: 100 }),
viewport,
})).toBe(false);
});
@@ -336,26 +359,33 @@ describe('is partially visible', () => {
});
describe('with invisible subject', () => {
- const frame: Spacing = {
- top: 10,
- left: 10,
- right: 100,
- bottom: 600,
- };
- const droppable: DroppableDimension = getDroppableDimension({
- descriptor: {
- id: 'clipped',
- type: 'TYPE',
- },
- client: getArea({
- ...frame,
- // smaller than frame
- bottom: 100,
- }),
- frameClient: getArea(frame),
- });
-
it('should return false when subject is totally invisible', () => {
+ // creating a droppable where the frame is bigger than the subject
+ const droppable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'droppable',
+ type: 'TYPE',
+ },
+ client: getArea({
+ top: 0,
+ left: 0,
+ bottom: 100,
+ right: 100,
+ }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ bottom: 100,
+ right: 100,
+ }),
+ scrollHeight: 600,
+ scrollWidth: 600,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
const originallyVisible: Spacing = {
...frame,
top: 10,
@@ -370,14 +400,19 @@ describe('is partially visible', () => {
})).toBe(true);
// subject is now totally invisible
- const scrolled: DroppableDimension = scrollDroppable(droppable, { x: 0, y: 101 });
+ const scrolled: DroppableDimension = scrollDroppable(
+ droppable,
+ getClosestScrollable(droppable).scroll.max,
+ );
+ // asserting frame is not visible
+ expect(scrolled.viewport.clipped).toBe(null);
+
+ // now asserting that this check will fail
expect(isPartiallyVisible({
target: originallyVisible,
destination: scrolled,
viewport,
})).toBe(false);
- // asserting frame is not visible
- expect(scrolled.viewport.clipped).toBe(null);
});
});
});
@@ -387,7 +422,7 @@ describe('is partially visible', () => {
expect(isPartiallyVisible({
target: inViewport1,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(true);
});
@@ -395,7 +430,7 @@ describe('is partially visible', () => {
expect(isPartiallyVisible({
target: inViewport2,
viewport,
- destination: smallDroppable,
+ destination: asBigAsInViewport1,
})).toBe(false);
});
diff --git a/test/unit/state/visibility/is-totally-visible-through-frame.spec.js b/test/unit/state/visibility/is-totally-visible-through-frame.spec.js
new file mode 100644
index 0000000000..d6621bbcd0
--- /dev/null
+++ b/test/unit/state/visibility/is-totally-visible-through-frame.spec.js
@@ -0,0 +1,82 @@
+// @flow
+import isTotallyVisibleThroughFrame from '../../../../src/state/visibility/is-totally-visible-through-frame';
+import { offsetByPosition, expandBySpacing } from '../../../../src/state/spacing';
+import type { Spacing } from '../../../../src/types';
+
+const frame: Spacing = {
+ top: 0, left: 0, right: 100, bottom: 100,
+};
+
+describe('is totally visible through frame', () => {
+ describe('subject is smaller than frame', () => {
+ describe('completely outside frame', () => {
+ it('should return false if subject is outside frame on any side', () => {
+ const outside: Spacing[] = [
+ // outside on top
+ offsetByPosition(frame, { x: 0, y: -101 }),
+ // outside on right
+ offsetByPosition(frame, { x: 101, y: 0 }),
+ // outside on bottom
+ offsetByPosition(frame, { x: 0, y: 101 }),
+ // outside on left
+ offsetByPosition(frame, { x: -101, y: 0 }),
+ ];
+
+ outside.forEach((subject: Spacing) => {
+ expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false);
+ });
+ });
+ });
+
+ describe('contained in frame', () => {
+ it('should return true when subject is contained within frame', () => {
+ const subject: Spacing = {
+ top: 10,
+ left: 10,
+ right: 90,
+ bottom: 90,
+ };
+
+ expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(true);
+ });
+ });
+
+ describe('partially visible', () => {
+ it('should return false if partially visible horizontally and vertically', () => {
+ const subject: Spacing = {
+ // visible
+ top: 10,
+ // not visible
+ bottom: 110,
+ // visible
+ left: 50,
+ // not visible
+ right: 150,
+ };
+
+ expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false);
+ });
+ });
+ });
+
+ describe('subject is equal to frame', () => {
+ it('should return true when the frame is equal to the subject', () => {
+ expect(isTotallyVisibleThroughFrame(frame)(frame)).toBe(true);
+ expect(isTotallyVisibleThroughFrame(frame)({ ...frame })).toBe(true);
+ });
+ });
+
+ describe('subject is bigger than frame', () => {
+ const bigSubject: Spacing = expandBySpacing(frame, frame);
+
+ it('should return false if the subject has no overlap with the frame', () => {
+ const subject: Spacing = offsetByPosition(bigSubject, { x: 1000, y: 1000 });
+
+ expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false);
+ });
+
+ it('should return false if subject is bigger on every side', () => {
+ expect(isTotallyVisibleThroughFrame(frame)(bigSubject)).toBe(false);
+ });
+ });
+});
diff --git a/test/unit/state/visibility/is-totally-visible.spec.js b/test/unit/state/visibility/is-totally-visible.spec.js
new file mode 100644
index 0000000000..6c64411121
--- /dev/null
+++ b/test/unit/state/visibility/is-totally-visible.spec.js
@@ -0,0 +1,470 @@
+// @flow
+import getArea from '../../../../src/state/get-area';
+import { isTotallyVisible, isPartiallyVisible } from '../../../../src/state/visibility/is-visible';
+import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension';
+import { offsetByPosition } from '../../../../src/state/spacing';
+import { getClosestScrollable } from '../../../utils/dimension';
+import type {
+ Area,
+ DroppableDimension,
+ Spacing,
+} from '../../../../src/types';
+
+const viewport: Area = getArea({
+ right: 800,
+ top: 0,
+ left: 0,
+ bottom: 600,
+});
+
+const asBigAsViewport: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'same-as-viewport',
+ type: 'TYPE',
+ },
+ client: viewport,
+});
+
+const inViewport1: Spacing = {
+ top: 10,
+ left: 10,
+ right: 100,
+ bottom: 100,
+};
+
+const inViewport2: Spacing = {
+ top: 10,
+ left: 200,
+ right: 400,
+ bottom: 100,
+};
+
+const notInViewport: Spacing = {
+ top: 0,
+ right: 1000,
+ left: 900,
+ bottom: 600,
+};
+
+const asBigAsInViewport1: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'subset',
+ type: 'TYPE',
+ },
+ client: getArea(inViewport1),
+});
+
+describe('is totally visible', () => {
+ describe('viewport', () => {
+ describe('without changes in droppable scroll', () => {
+ it('should return false if the item is not in the viewport', () => {
+ expect(isTotallyVisible({
+ target: notInViewport,
+ viewport,
+ destination: asBigAsViewport,
+ })).toBe(false);
+ });
+
+ it('should return true if item takes up entire viewport', () => {
+ expect(isTotallyVisible({
+ target: viewport,
+ viewport,
+ destination: asBigAsViewport,
+ })).toBe(true);
+ });
+
+ it('should return true if the item is totally visible in the viewport', () => {
+ expect(isTotallyVisible({
+ target: inViewport1,
+ viewport,
+ destination: asBigAsViewport,
+ })).toBe(true);
+ });
+
+ it('should return false if the item is partially visible in the viewport', () => {
+ const partials: Spacing[] = [
+ // bleed over top
+ offsetByPosition(viewport, { x: 0, y: -1 }),
+ // bleed over right
+ offsetByPosition(viewport, { x: 1, y: 0 }),
+ // bleed over bottom
+ offsetByPosition(viewport, { x: 0, y: 1 }),
+ // bleed over left
+ offsetByPosition(viewport, { x: -1, y: 0 }),
+ ];
+
+ partials.forEach((partial: Spacing) => {
+ expect(isTotallyVisible({
+ target: partial,
+ viewport,
+ destination: asBigAsViewport,
+ })).toBe(false);
+
+ // validation
+ expect(isPartiallyVisible({
+ target: partial,
+ viewport,
+ destination: asBigAsViewport,
+ })).toBe(true);
+ });
+ });
+ });
+
+ describe('with changes in droppable scroll', () => {
+ const clippedByViewport: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'clipped',
+ type: 'TYPE',
+ },
+ client: getArea({
+ top: viewport.top,
+ // stretches out the bottom of the viewport
+ bottom: viewport.bottom + 100,
+ left: viewport.left,
+ right: viewport.right,
+ }),
+ closest: {
+ frameClient: viewport,
+ scrollWidth: viewport.width,
+ scrollHeight: viewport.bottom + 100,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ describe('originally invisible but now invisible', () => {
+ it('should take into account the droppable scroll when detecting visibility', () => {
+ const originallyInvisible: Spacing = {
+ top: viewport.bottom + 1,
+ bottom: viewport.bottom + 100,
+ right: viewport.left + 1,
+ left: viewport.left + 100,
+ };
+
+ // originally invisible
+ expect(isTotallyVisible({
+ target: originallyInvisible,
+ destination: clippedByViewport,
+ viewport,
+ })).toBe(false);
+
+ // after scroll the target is now visible
+ expect(isTotallyVisible({
+ target: originallyInvisible,
+ destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }),
+ viewport,
+ })).toBe(true);
+ });
+ });
+
+ describe('originally visible but now visible', () => {
+ it('should take into account the droppable scroll when detecting visibility', () => {
+ const originallyVisible: Spacing = {
+ top: viewport.top,
+ bottom: viewport.top + 50,
+ right: viewport.left + 1,
+ left: viewport.left + 100,
+ };
+
+ // originally visible
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: clippedByViewport,
+ viewport,
+ })).toBe(true);
+
+ // after scroll the target is now invisible
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }),
+ viewport,
+ })).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('droppable', () => {
+ const client: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 600,
+ bottom: 600,
+ });
+ const frame: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+ });
+
+ const scrollable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'clipped',
+ type: 'TYPE',
+ },
+ client,
+ closest: {
+ frameClient: frame,
+ scrollHeight: client.height,
+ scrollWidth: client.width,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ describe('without changes in droppable scroll', () => {
+ it('should return false if outside the droppable', () => {
+ expect(isTotallyVisible({
+ target: inViewport2,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(false);
+ });
+
+ it('should return false if the target is bigger than the droppable', () => {
+ expect(isTotallyVisible({
+ target: viewport,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(false);
+ });
+
+ it('should return true if the same size of the droppable', () => {
+ expect(isTotallyVisible({
+ target: inViewport1,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(true);
+ });
+
+ it('should return true if within the droppable', () => {
+ const insideDroppable: Spacing = {
+ top: 20,
+ left: 20,
+ right: 80,
+ bottom: 80,
+ };
+
+ expect(isTotallyVisible({
+ target: insideDroppable,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(true);
+ });
+
+ it('should return false if partially within the droppable', () => {
+ const partials: Spacing[] = [
+ // bleed over top
+ offsetByPosition(inViewport1, { x: 0, y: -1 }),
+ // bleed over right
+ offsetByPosition(inViewport1, { x: 1, y: 0 }),
+ // bleed over bottom
+ offsetByPosition(inViewport1, { x: 0, y: 1 }),
+ // bleed over left
+ offsetByPosition(inViewport1, { x: -1, y: 0 }),
+ ];
+
+ partials.forEach((partial: Spacing) => {
+ expect(isTotallyVisible({
+ target: partial,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(false);
+
+ // validation
+ expect(isPartiallyVisible({
+ target: partial,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(true);
+ });
+ });
+
+ it('should return false if falling on clipped area of droppable', () => {
+ const ourFrame: Spacing = {
+ top: 10,
+ left: 10,
+ right: 100,
+ // cuts the droppable short
+ bottom: 100,
+ };
+ const clippedDroppable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'clipped',
+ type: 'TYPE',
+ },
+ client: getArea({
+ ...ourFrame,
+ // stretches out past frame
+ bottom: 600,
+ }),
+ closest: {
+ frameClient: getArea(ourFrame),
+ scrollHeight: 600,
+ scrollWidth: getArea(ourFrame).width,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+ const inSubjectOutsideFrame: Spacing = {
+ ...frame,
+ top: 110,
+ bottom: 200,
+ };
+
+ expect(isTotallyVisible({
+ target: inSubjectOutsideFrame,
+ destination: clippedDroppable,
+ viewport,
+ })).toBe(false);
+ });
+ });
+
+ describe('with changes in droppable scroll', () => {
+ describe('originally invisible but now invisible', () => {
+ it('should take into account the droppable scroll when detecting visibility', () => {
+ const originallyInvisible: Spacing = {
+ left: frame.left,
+ right: frame.right,
+ top: frame.bottom + 10,
+ bottom: frame.bottom + 20,
+ };
+
+ // originally invisible
+ expect(isTotallyVisible({
+ target: originallyInvisible,
+ destination: scrollable,
+ viewport,
+ })).toBe(false);
+
+ // after scroll the target is now visible
+ const scrolled: DroppableDimension = scrollDroppable(scrollable, { x: 0, y: 100 });
+ expect(isTotallyVisible({
+ target: originallyInvisible,
+ destination: scrolled,
+ viewport,
+ })).toBe(true);
+ });
+ });
+
+ describe('originally visible but now visible', () => {
+ it('should take into account the droppable scroll when detecting visibility', () => {
+ const originallyVisible: Spacing = {
+ ...frame,
+ top: 10,
+ bottom: 20,
+ };
+
+ // originally visible
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: scrollable,
+ viewport,
+ })).toBe(true);
+
+ // after scroll the target is now invisible
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: scrollDroppable(scrollable, { x: 0, y: 100 }),
+ viewport,
+ })).toBe(false);
+ });
+ });
+ });
+
+ describe('with invisible subject', () => {
+ it('should return false when subject is totally invisible', () => {
+ // creating a droppable where the frame is bigger than the subject
+ const droppable: DroppableDimension = getDroppableDimension({
+ descriptor: {
+ id: 'droppable',
+ type: 'TYPE',
+ },
+ client: getArea({
+ top: 0,
+ left: 0,
+ bottom: 100,
+ right: 100,
+ }),
+ closest: {
+ frameClient: getArea({
+ top: 0,
+ left: 0,
+ bottom: 100,
+ right: 100,
+ }),
+ scrollHeight: 600,
+ scrollWidth: 600,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ const originallyVisible: Spacing = {
+ ...frame,
+ top: 10,
+ bottom: 20,
+ };
+
+ // originally visible
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: droppable,
+ viewport,
+ })).toBe(true);
+
+ // subject is now totally invisible
+ const scrolled: DroppableDimension = scrollDroppable(
+ droppable,
+ getClosestScrollable(droppable).scroll.max,
+ );
+ // asserting frame is not visible
+ expect(scrolled.viewport.clipped).toBe(null);
+
+ // now asserting that this check will fail
+ expect(isTotallyVisible({
+ target: originallyVisible,
+ destination: scrolled,
+ viewport,
+ })).toBe(false);
+ });
+ });
+ });
+
+ describe('viewport + droppable', () => {
+ it('should return true if visible in the viewport and the droppable', () => {
+ expect(isTotallyVisible({
+ target: inViewport1,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(true);
+ });
+
+ it('should return false if not visible in the droppable even if visible in the viewport', () => {
+ expect(isTotallyVisible({
+ target: inViewport2,
+ viewport,
+ destination: asBigAsInViewport1,
+ })).toBe(false);
+ });
+
+ it('should return false if not visible in the viewport even if visible in the droppable', () => {
+ const notVisibleDroppable = getDroppableDimension({
+ descriptor: {
+ id: 'not-visible',
+ type: 'TYPE',
+ },
+ client: getArea(notInViewport),
+ });
+
+ expect(isTotallyVisible({
+ // is visibile in the droppable
+ target: notInViewport,
+ // but not visible in the viewport
+ viewport,
+ destination: notVisibleDroppable,
+ })).toBe(false);
+ });
+ });
+});
diff --git a/test/unit/view/annoucer.spec.js b/test/unit/view/annoucer.spec.js
new file mode 100644
index 0000000000..114d9685c2
--- /dev/null
+++ b/test/unit/view/annoucer.spec.js
@@ -0,0 +1,107 @@
+// @flow
+import createAnnouncer from '../../../src/view/announcer/announcer';
+import type { Announcer } from '../../../src/view/announcer/announcer-types';
+
+describe('announcer', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => { });
+ });
+
+ afterEach(() => {
+ console.error.mockRestore();
+ });
+
+ describe('mounting', () => {
+ it('should not create a dom node before mount is called', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ const el: ?HTMLElement = document.getElementById(announcer.id);
+
+ expect(el).not.toBeTruthy();
+ });
+
+ it('should create a new element when mounting', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.mount();
+ const el: ?HTMLElement = document.getElementById(announcer.id);
+
+ expect(el).toBeInstanceOf(HTMLElement);
+ });
+
+ it('should error if attempting to double mount', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.mount();
+ expect(console.error).not.toHaveBeenCalled();
+
+ announcer.mount();
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should apply the appropriate aria attributes and non visibility styles', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.mount();
+ const el: HTMLElement = (document.getElementById(announcer.id) : any);
+
+ expect(el.getAttribute('aria-live')).toBe('assertive');
+ expect(el.getAttribute('role')).toBe('log');
+ expect(el.getAttribute('aria-atomic')).toBe('true');
+
+ // not checking all the styles - just enough to know we are doing something
+ expect(el.style.overflow).toBe('hidden');
+ });
+ });
+
+ describe('unmounting', () => {
+ it('should remove the element when unmounting', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.mount();
+ announcer.unmount();
+ const el: ?HTMLElement = document.getElementById(announcer.id);
+
+ expect(el).not.toBeTruthy();
+ });
+
+ it('should error if attempting to unmount before mounting', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.unmount();
+
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should error if unmounting after an unmount', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.mount();
+ announcer.unmount();
+ expect(console.error).not.toHaveBeenCalled();
+
+ announcer.unmount();
+ expect(console.error).toHaveBeenCalled();
+ });
+ });
+
+ describe('announcing', () => {
+ it('should error if not mounted', () => {
+ const announcer: Announcer = createAnnouncer();
+
+ announcer.announce('test');
+
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should set the text content of the announcement element', () => {
+ const announcer: Announcer = createAnnouncer();
+ announcer.mount();
+ const el: HTMLElement = (document.getElementById(announcer.id) : any);
+
+ announcer.announce('test');
+
+ expect(el.textContent).toBe('test');
+ });
+ });
+});
diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js
index b5b7f6280d..df521e5880 100644
--- a/test/unit/view/connected-draggable.spec.js
+++ b/test/unit/view/connected-draggable.spec.js
@@ -3,10 +3,10 @@
import React, { Component } from 'react';
import { mount } from 'enzyme';
import Draggable, { makeSelector } from '../../../src/view/draggable/connected-draggable';
-import { getPreset } from '../../utils/dimension';
+import { getPreset, getInitialImpact, withImpact } from '../../utils/dimension';
import { negate } from '../../../src/state/position';
import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal';
-import * as state from '../../utils/simple-state-preset';
+import getStatePreset from '../../utils/get-simple-state-preset';
import {
combine,
withStore,
@@ -29,9 +29,11 @@ import type {
CurrentDragPositions,
DragImpact,
DraggableDimension,
+ DraggableLocation,
} from '../../../src/types';
const preset = getPreset();
+const state = getStatePreset();
const move = (previous: State, offset: Position): State => {
const clientPositions: CurrentDragPositions = {
offset,
@@ -88,153 +90,304 @@ describe('Connected Draggable', () => {
expect(console.error).toHaveBeenCalled();
});
- it('should move the dragging item to the current offset', () => {
- const selector: Selector = makeSelector();
+ describe('is not over a droppable', () => {
+ it('should move the dragging item to the current offset', () => {
+ const selector: Selector = makeSelector();
+
+ const result: MapProps = selector(
+ move(state.dragging(), { x: 20, y: 30 }),
+ ownProps
+ );
- const result: MapProps = selector(
- move(state.dragging(), { x: 20, y: 30 }),
- ownProps
- );
-
- expect(result).toEqual({
- isDropAnimating: false,
- isDragging: true,
- offset: { x: 20, y: 30 },
- shouldAnimateDragMovement: false,
- shouldAnimateDisplacement: false,
- dimension: preset.inHome1,
- direction: null,
+ expect(result).toEqual({
+ isDropAnimating: false,
+ isDragging: true,
+ offset: { x: 20, y: 30 },
+ shouldAnimateDragMovement: false,
+ shouldAnimateDisplacement: false,
+ dimension: preset.inHome1,
+ direction: null,
+ draggingOver: null,
+ });
});
- });
- it('should control whether drag movement is allowed based the current state', () => {
- const selector: Selector = makeSelector();
- const previous: State = move(state.dragging(), { x: 20, y: 30 });
-
- // drag animation is allowed
- const allowed: State = {
- ...previous,
- drag: {
- ...previous.drag,
- current: {
- // $ExpectError - not checking for null
- ...previous.drag.current,
- shouldAnimate: true,
- },
- },
- };
- expect(selector(allowed, ownProps).shouldAnimateDragMovement).toBe(true);
-
- // drag animation is not allowed
- const notAllowed: State = {
- ...previous,
- drag: {
- ...previous.drag,
- current: {
- // $ExpectError - not checking for null
- ...previous.drag.current,
- shouldAnimate: false,
+ it('should control whether drag movement is allowed based the current state', () => {
+ const selector: Selector = makeSelector();
+ const previous: State = move(state.dragging(), { x: 20, y: 30 });
+
+ // drag animation is allowed
+ const allowed: State = {
+ ...previous,
+ drag: {
+ ...previous.drag,
+ current: {
+ // $ExpectError - not checking for null
+ ...previous.drag.current,
+ shouldAnimate: true,
+ },
},
- },
- };
- expect(selector(notAllowed, ownProps).shouldAnimateDragMovement).toBe(false);
- });
+ };
+ expect(selector(allowed, ownProps).shouldAnimateDragMovement).toBe(true);
- it('should not break memoization on multiple calls to the same offset', () => {
- const selector: Selector = makeSelector();
+ // drag animation is not allowed
+ const notAllowed: State = {
+ ...previous,
+ drag: {
+ ...previous.drag,
+ current: {
+ // $ExpectError - not checking for null
+ ...previous.drag.current,
+ shouldAnimate: false,
+ },
+ },
+ };
+ expect(selector(notAllowed, ownProps).shouldAnimateDragMovement).toBe(false);
+ });
- const result1: MapProps = selector(
- move(state.dragging(), { x: 100, y: 200 }),
- ownProps
- );
- const result2: MapProps = selector(
- move(state.dragging(), { x: 100, y: 200 }),
- ownProps
- );
-
- expect(result1).toBe(result2);
- expect(selector.recomputations()).toBe(1);
- });
+ it('should not break memoization on multiple calls to the same offset', () => {
+ const selector: Selector = makeSelector();
- it('should break memoization on multiple calls if moving to a new position', () => {
- const selector: Selector = makeSelector();
+ const result1: MapProps = selector(
+ move(state.dragging(), { x: 100, y: 200 }),
+ ownProps
+ );
+ const result2: MapProps = selector(
+ move(state.dragging(), { x: 100, y: 200 }),
+ ownProps
+ );
- const result1: MapProps = selector(
- move(state.dragging(), { x: 100, y: 200 }),
- ownProps
- );
- const result2: MapProps = selector(
- move({ ...state.dragging() }, { x: 101, y: 200 }),
- ownProps
- );
-
- expect(result1).not.toBe(result2);
- expect(result1).not.toEqual(result2);
- expect(selector.recomputations()).toBe(2);
- });
+ expect(result1).toBe(result2);
+ expect(selector.recomputations()).toBe(1);
+ });
- describe('drop animating', () => {
- it('should log an error when there is invalid drag state', () => {
- const invalid: State = {
- ...state.dropAnimating(),
- drop: null,
- };
+ it('should break memoization on multiple calls if moving to a new position', () => {
const selector: Selector = makeSelector();
- const defaultMapProps: MapProps = selector(state.idle, ownProps);
- const result: MapProps = selector(invalid, ownProps);
+ const result1: MapProps = selector(
+ move(state.dragging(), { x: 100, y: 200 }),
+ ownProps
+ );
+ const result2: MapProps = selector(
+ move({ ...state.dragging() }, { x: 101, y: 200 }),
+ ownProps
+ );
- expect(result).toBe(defaultMapProps);
- expect(console.error).toHaveBeenCalled();
+ expect(result1).not.toBe(result2);
+ expect(result1).not.toEqual(result2);
+ expect(selector.recomputations()).toBe(2);
});
- it('should move the draggable to the new offset', () => {
+ describe('drop animating', () => {
+ it('should log an error when there is invalid drag state', () => {
+ const invalid: State = {
+ ...state.dropAnimating(),
+ drop: null,
+ };
+ const selector: Selector = makeSelector();
+ const defaultMapProps: MapProps = selector(state.idle, ownProps);
+
+ const result: MapProps = selector(invalid, ownProps);
+
+ expect(result).toBe(defaultMapProps);
+ expect(console.error).toHaveBeenCalled();
+ });
+
+ it('should move the draggable to the new offset', () => {
+ const selector: Selector = makeSelector();
+ const current: State = state.dropAnimating();
+
+ const result: MapProps = selector(
+ current,
+ ownProps,
+ );
+
+ expect(result).toEqual({
+ // no longer dragging
+ isDragging: false,
+ // is now drop animating
+ isDropAnimating: true,
+ // $ExpectError - not testing for null
+ offset: current.drop.pending.newHomeOffset,
+ dimension: preset.inHome1,
+ direction: null,
+ // animation now controlled by isDropAnimating flag
+ shouldAnimateDisplacement: false,
+ shouldAnimateDragMovement: false,
+ draggingOver: null,
+ });
+ });
+ });
+
+ describe('user cancel', () => {
+ it('should move the draggable to the new offset', () => {
+ const selector: Selector = makeSelector();
+ const current: State = state.userCancel();
+
+ const result: MapProps = selector(
+ current,
+ ownProps,
+ );
+
+ expect(result).toEqual({
+ // no longer dragging
+ isDragging: false,
+ // is now drop animating
+ isDropAnimating: true,
+ // $ExpectError - not testing for null
+ offset: current.drop.pending.newHomeOffset,
+ dimension: preset.inHome1,
+ direction: null,
+ // animation now controlled by isDropAnimating flag
+ shouldAnimateDisplacement: false,
+ shouldAnimateDragMovement: false,
+ draggingOver: null,
+ });
+ });
+ });
+ });
+
+ describe('is over a droppable (test subset)', () => {
+ it('should move the dragging item to the current offset', () => {
const selector: Selector = makeSelector();
- const current: State = state.dropAnimating();
const result: MapProps = selector(
- current,
- ownProps,
+ withImpact(
+ move(state.dragging(), { x: 20, y: 30 }),
+ getInitialImpact(preset.inHome1),
+ ),
+ ownProps
);
expect(result).toEqual({
- // no longer dragging
- isDragging: false,
- // is now drop animating
- isDropAnimating: true,
- // $ExpectError - not testing for null
- offset: current.drop.pending.newHomeOffset,
- dimension: preset.inHome1,
- direction: null,
- // animation now controlled by isDropAnimating flag
- shouldAnimateDisplacement: false,
+ isDropAnimating: false,
+ isDragging: true,
+ offset: { x: 20, y: 30 },
shouldAnimateDragMovement: false,
+ shouldAnimateDisplacement: false,
+ dimension: preset.inHome1,
+ direction: preset.home.axis.direction,
+ draggingOver: preset.home.descriptor.id,
});
});
- });
- describe('user cancel', () => {
- it('should move the draggable to the new offset', () => {
+ it('should not break memoization on multiple calls to the same offset', () => {
const selector: Selector = makeSelector();
- const current: State = state.userCancel();
- const result: MapProps = selector(
- current,
- ownProps,
+ const result1: MapProps = selector(
+ withImpact(
+ move(state.dragging(), { x: 100, y: 200 }),
+ getInitialImpact(preset.inHome1),
+ ),
+ ownProps
+ );
+ const result2: MapProps = selector(
+ withImpact(
+ move(state.dragging(), { x: 100, y: 200 }),
+ getInitialImpact(preset.inHome1),
+ ),
+ ownProps
);
- expect(result).toEqual({
- // no longer dragging
- isDragging: false,
- // is now drop animating
- isDropAnimating: true,
- // $ExpectError - not testing for null
- offset: current.drop.pending.newHomeOffset,
- dimension: preset.inHome1,
- direction: null,
- // animation now controlled by isDropAnimating flag
- shouldAnimateDisplacement: false,
- shouldAnimateDragMovement: false,
+ expect(result1).toBe(result2);
+ expect(selector.recomputations()).toBe(1);
+ });
+
+ it('should break memoization on multiple calls if moving to a new position', () => {
+ const selector: Selector = makeSelector();
+
+ const result1: MapProps = selector(
+ withImpact(
+ move(state.dragging(), { x: 100, y: 200 }),
+ getInitialImpact(preset.inHome1)
+ ),
+ ownProps
+ );
+ const result2: MapProps = selector(
+ withImpact(
+ move({ ...state.dragging() }, { x: 101, y: 200 }),
+ getInitialImpact(preset.inHome1),
+ ),
+ ownProps
+ );
+
+ expect(result1).not.toBe(result2);
+ expect(result1).not.toEqual(result2);
+ expect(selector.recomputations()).toBe(2);
+ });
+
+ describe('drop animating', () => {
+ it('should move the draggable to the new offset', () => {
+ const selector: Selector = makeSelector();
+ const destination: DraggableLocation = {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const current: State = withImpact(
+ state.dropAnimating(),
+ getInitialImpact(preset.inHome1)
+ );
+ if (!current.drop || !current.drop.pending) {
+ throw new Error('invalid test setup');
+ }
+ current.drop.pending.result.destination = destination;
+
+ const result: MapProps = selector(
+ current,
+ ownProps,
+ );
+
+ expect(result).toEqual({
+ // no longer dragging
+ isDragging: false,
+ // is now drop animating
+ isDropAnimating: true,
+ // $ExpectError - not testing for null
+ offset: current.drop.pending.newHomeOffset,
+ dimension: preset.inHome1,
+ direction: preset.home.axis.direction,
+ draggingOver: preset.home.descriptor.id,
+ // animation now controlled by isDropAnimating flag
+ shouldAnimateDisplacement: false,
+ shouldAnimateDragMovement: false,
+ });
+ });
+ });
+
+ describe('user cancel', () => {
+ it('should move the draggable to the new offset', () => {
+ const selector: Selector = makeSelector();
+ const destination: DraggableLocation = {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ };
+ const current: State = withImpact(
+ state.userCancel(),
+ getInitialImpact(preset.inHome1)
+ );
+ if (!current.drop || !current.drop.pending) {
+ throw new Error('invalid test setup');
+ }
+ current.drop.pending.result.destination = destination;
+
+ const result: MapProps = selector(
+ current,
+ ownProps,
+ );
+
+ expect(result).toEqual({
+ // no longer dragging
+ isDragging: false,
+ // is now drop animating
+ isDropAnimating: true,
+ // $ExpectError - not testing for null
+ offset: current.drop.pending.newHomeOffset,
+ dimension: preset.inHome1,
+ direction: preset.home.axis.direction,
+ draggingOver: preset.home.descriptor.id,
+ // animation now controlled by isDropAnimating flag
+ shouldAnimateDisplacement: false,
+ shouldAnimateDragMovement: false,
+ });
});
});
});
@@ -452,6 +605,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
@@ -499,6 +653,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
@@ -585,6 +740,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
@@ -669,6 +825,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
@@ -754,6 +911,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
});
@@ -840,6 +998,7 @@ describe('Connected Draggable', () => {
shouldAnimateDragMovement: false,
dimension: null,
direction: null,
+ draggingOver: null,
});
});
});
@@ -888,10 +1047,11 @@ describe('Connected Draggable', () => {
// so that the draggable can publish itself
const marshal: DimensionMarshal = createDimensionMarshal({
cancel: () => { },
- publishDraggables: () => { },
- publishDroppables: () => { },
+ publishDraggable: () => { },
+ publishDroppable: () => { },
updateDroppableScroll: () => { },
updateDroppableIsEnabled: () => { },
+ bulkPublish: () => { },
});
const options: Object = combine(
withStore(),
@@ -907,6 +1067,7 @@ describe('Connected Draggable', () => {
getDimension: () => preset.home,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => { },
});
class Person extends Component<{ name: string, provided: Provided}> {
diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js
index 92bb51f510..da04623065 100644
--- a/test/unit/view/connected-droppable.spec.js
+++ b/test/unit/view/connected-droppable.spec.js
@@ -2,9 +2,9 @@
/* eslint-disable react/no-multi-comp */
import React, { Component } from 'react';
import { mount } from 'enzyme';
-import { withStore, combine, withDimensionMarshal } from '../../utils/get-context-options';
+import { withStore, combine, withDimensionMarshal, withStyleContext } from '../../utils/get-context-options';
import Droppable, { makeSelector } from '../../../src/view/droppable/connected-droppable';
-import * as state from '../../utils/simple-state-preset';
+import getStatePreset from '../../utils/get-simple-state-preset';
import forceUpdate from '../../utils/force-update';
import { getPreset } from '../../utils/dimension';
import type {
@@ -20,6 +20,7 @@ import type {
} from '../../../src/types';
const preset = getPreset();
+const state = getStatePreset();
const clone = (value: State): State => (JSON.parse(JSON.stringify(value)) : any);
const getOwnProps = (dimension: DroppableDimension): OwnProps => ({
@@ -77,6 +78,7 @@ describe('Connected Droppable', () => {
const selector: Selector = makeSelector();
const expected: MapProps = {
isDraggingOver: false,
+ draggingOverWith: null,
placeholder: null,
};
@@ -90,6 +92,7 @@ describe('Connected Droppable', () => {
const selector: Selector = makeSelector();
const expected: MapProps = {
isDraggingOver: false,
+ draggingOverWith: null,
placeholder: null,
};
@@ -153,6 +156,7 @@ describe('Connected Droppable', () => {
expect(result).toEqual({
isDraggingOver: true,
+ draggingOverWith: preset.inHome1.descriptor.id,
placeholder: null,
});
});
@@ -198,6 +202,7 @@ describe('Connected Droppable', () => {
const selector: Selector = makeSelector();
const expected: MapProps = {
isDraggingOver: true,
+ draggingOverWith: preset.inHome1.descriptor.id,
placeholder: preset.inHome1.placeholder,
};
@@ -265,7 +270,6 @@ describe('Connected Droppable', () => {
drop: {
result: null,
pending: {
- trigger: 'DROP',
newHomeOffset: { x: 0, y: 0 },
impact,
result: {
@@ -279,6 +283,7 @@ describe('Connected Droppable', () => {
index: preset.inHome1.descriptor.index,
droppableId: preset.home.descriptor.id,
},
+ reason: 'DROP',
},
},
},
@@ -342,7 +347,6 @@ describe('Connected Droppable', () => {
drop: {
result: null,
pending: {
- trigger: 'DROP',
newHomeOffset: { x: 0, y: 0 },
impact,
result: {
@@ -353,6 +357,7 @@ describe('Connected Droppable', () => {
droppableId: preset.home.descriptor.id,
},
destination: impact.destination,
+ reason: 'DROP',
},
},
},
@@ -389,13 +394,17 @@ describe('Connected Droppable', () => {
});
describe('child render behavior', () => {
- const contextOptions = combine(withStore(), withDimensionMarshal());
+ const contextOptions = combine(
+ withStore(),
+ withDimensionMarshal(),
+ withStyleContext(),
+ );
class Person extends Component<{ name: string, provided: Provided }> {
render() {
const { provided, name } = this.props;
return (
-
provided.innerRef(ref)}>
+
provided.innerRef(ref)} {...provided.droppableProps}>
hello {name}
);
diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js
index 1a2c5a5833..8266a90ca1 100644
--- a/test/unit/view/dimension-marshal.spec.js
+++ b/test/unit/view/dimension-marshal.spec.js
@@ -3,7 +3,7 @@ import { getPreset } from '../../utils/dimension';
import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal';
import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension';
import getArea from '../../../src/state/get-area';
-import * as state from '../../utils/simple-state-preset';
+import getStatePreset from '../../utils/get-simple-state-preset';
import type {
Callbacks,
DimensionMarshal,
@@ -20,14 +20,16 @@ import type {
DroppableId,
DraggableDescriptor,
DroppableDescriptor,
+ ScrollOptions,
Area,
} from '../../../src/types';
const getCallbackStub = (): Callbacks => {
const callbacks: Callbacks = {
cancel: jest.fn(),
- publishDraggables: jest.fn(),
- publishDroppables: jest.fn(),
+ publishDraggable: jest.fn(),
+ publishDroppable: jest.fn(),
+ bulkPublish: jest.fn(),
updateDroppableScroll: jest.fn(),
updateDroppableIsEnabled: jest.fn(),
};
@@ -40,6 +42,7 @@ type PopulateMarshalState = {|
|}
const preset = getPreset();
+const state = getStatePreset();
const defaultArgs: PopulateMarshalState = {
draggables: preset.draggables,
@@ -80,12 +83,13 @@ const populateMarshal = (
watches.droppable.getDimension(id);
return droppable;
},
- watchScroll: () => {
- watches.droppable.watchScroll(id);
+ watchScroll: (options: ScrollOptions) => {
+ watches.droppable.watchScroll(id, options);
},
unwatchScroll: () => {
watches.droppable.unwatchScroll(id);
},
+ scroll: () => {},
};
marshal.registerDroppable(droppable.descriptor, callbacks);
@@ -123,6 +127,30 @@ const childOfAnotherType: DraggableDimension = getDraggableDimension({
client: fakeArea,
});
+const immediate: ScrollOptions = {
+ shouldPublishImmediately: true,
+};
+const scheduled: ScrollOptions = {
+ shouldPublishImmediately: false,
+};
+
+const withScrollOptions = (current: State, scrollOptions: ScrollOptions) => {
+ if (!current.dimension.request) {
+ throw new Error('Invalid test setup');
+ }
+
+ return {
+ ...current,
+ dimension: {
+ ...current.dimension,
+ request: {
+ ...current.dimension.request,
+ scrollOptions,
+ },
+ },
+ };
+};
+
describe('dimension marshal', () => {
beforeAll(() => {
requestAnimationFrame.reset();
@@ -159,7 +187,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting('some-unknown-descriptor'));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: 'some-unknown-descriptor',
+ scrollOptions: scheduled,
+ }));
expect(callbacks.cancel).toHaveBeenCalled();
});
@@ -178,7 +209,10 @@ describe('dimension marshal', () => {
});
// there is now no published home droppable
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
expect(callbacks.cancel).toHaveBeenCalled();
});
@@ -190,24 +224,52 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toBeCalledWith([preset.inHome1]);
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDroppables).toBeCalledWith([preset.home]);
+ expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDraggable).toBeCalledWith(preset.inHome1);
+ expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDroppable).toBeCalledWith(preset.home);
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
});
- it('should ask the home droppable to start listening to scrolling', () => {
+ it('should ask the home droppable to start listening to scrolling (scheduled scroll)', () => {
const callbacks = getCallbackStub();
const marshal = createDimensionMarshal(callbacks);
const watches = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// it should not watch scroll on the other droppables at this stage
expect(watches.droppable.watchScroll).toHaveBeenCalledTimes(1);
- expect(watches.droppable.watchScroll).toHaveBeenCalledWith(preset.home.descriptor.id);
+ expect(watches.droppable.watchScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id,
+ scheduled,
+ );
+ });
+
+ it('should ask the home droppable to start listening to scrolling (immediate scroll)', () => {
+ const callbacks = getCallbackStub();
+ const marshal = createDimensionMarshal(callbacks);
+ const watches = populateMarshal(marshal);
+
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: immediate,
+ }));
+
+ // it should not watch scroll on the other droppables at this stage
+ expect(watches.droppable.watchScroll).toHaveBeenCalledTimes(1);
+ expect(watches.droppable.watchScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id,
+ immediate,
+ );
});
});
@@ -218,9 +280,14 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
+ expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1);
+ callbacks.publishDraggable.mockReset();
+ callbacks.publishDroppable.mockReset();
// moving to idle state before moving to dragging state
marshal.onPhaseChange(state.idle);
@@ -229,8 +296,9 @@ describe('dimension marshal', () => {
requestAnimationFrame.flush();
// nothing happened
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
+ expect(callbacks.publishDraggable).not.toHaveBeenCalled();
});
});
@@ -240,11 +308,15 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
const watchers = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
- callbacks.publishDroppables.mockClear();
- callbacks.publishDraggables.mockClear();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
+ expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1);
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
+ callbacks.publishDroppable.mockClear();
+ callbacks.publishDraggable.mockClear();
watchers.draggable.getDimension.mockClear();
watchers.droppable.getDimension.mockClear();
@@ -255,7 +327,7 @@ describe('dimension marshal', () => {
// flush all timers - would normally collect and publish
requestAnimationFrame.flush();
- expect(callbacks.publishDraggables).not.toHaveBeenCalled();
+ expect(callbacks.publishDraggable).not.toHaveBeenCalled();
expect(watchers.droppable.getDimension).not.toHaveBeenCalled();
});
@@ -264,7 +336,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
const watchers = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(1);
expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(1);
watchers.draggable.getDimension.mockClear();
@@ -297,7 +372,10 @@ describe('dimension marshal', () => {
droppables,
});
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// clearing the initial calls
watchers.draggable.getDimension.mockClear();
watchers.droppable.getDimension.mockClear();
@@ -323,7 +401,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
const watchers = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// called straight away
expect(watchers.draggable.getDimension)
@@ -345,7 +426,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
const watchers = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// called straight away
expect(watchers.droppable.getDimension)
@@ -367,7 +451,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
const watchers = populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// called straight away
expect(watchers.droppable.getDimension)
@@ -395,10 +482,13 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// clearing initial calls
- callbacks.publishDraggables.mockClear();
- callbacks.publishDroppables.mockClear();
+ callbacks.publishDraggable.mockClear();
+ callbacks.publishDroppable.mockClear();
// execute collection frame
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
@@ -409,83 +499,97 @@ describe('dimension marshal', () => {
requestAnimationFrame.step();
// nothing additional called
- expect(callbacks.publishDraggables).not.toHaveBeenCalled();
- expect(callbacks.publishDroppables).not.toHaveBeenCalled();
+ expect(callbacks.publishDraggable).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
});
- it('should publish all the collected draggables', () => {
+ it('should publish all the collected droppables', () => {
const callbacks = getCallbackStub();
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- // clearing initial calls
- callbacks.publishDraggables.mockClear();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
// calls are batched
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
- const result: DraggableDimension[] = callbacks.publishDraggables.mock.calls[0][0];
+ expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1);
+ const result: DroppableDimension[] = callbacks.bulkPublish.mock.calls[0][0];
// not calling for the dragging item
- expect(result.length).toBe(Object.keys(preset.draggables).length - 1);
+ expect(result.length).toBe(Object.keys(preset.droppables).length - 1);
// super explicit test
// - doing it like this because the order of Object.keys is not guarenteed
- Object.keys(preset.draggables).forEach((id: DraggableId) => {
- if (id === preset.inHome1.descriptor.id) {
- expect(result).not.toContain(preset.inHome1);
+ Object.keys(preset.droppables).forEach((id: DroppableId) => {
+ if (id === preset.home.descriptor.id) {
+ expect(result.includes(preset.home)).toBe(false);
return;
}
- expect(result).toContain(preset.draggables[id]);
+ expect(result.includes(preset.droppables[id])).toBe(true);
});
});
- it('should publish all the collected droppables', () => {
+ it('should publish all the collected draggables', () => {
const callbacks = getCallbackStub();
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- // clearing initial calls
- callbacks.publishDroppables.mockClear();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
// calls are batched
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- const result: DroppableDimension[] = callbacks.publishDroppables.mock.calls[0][0];
+ expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1);
+ const result: DraggableDimension[] = callbacks.bulkPublish.mock.calls[0][1];
+
// not calling for the dragging item
- expect(result.length).toBe(Object.keys(preset.droppables).length - 1);
+ expect(result.length).toBe(Object.keys(preset.draggables).length - 1);
+
// super explicit test
// - doing it like this because the order of Object.keys is not guarenteed
- Object.keys(preset.droppables).forEach((id: DroppableId) => {
- if (id === preset.home.descriptor.id) {
- expect(result.includes(preset.home)).toBe(false);
+ Object.keys(preset.draggables).forEach((id: DraggableId) => {
+ if (id === preset.inHome1.descriptor.id) {
+ expect(result).not.toContain(preset.inHome1);
return;
}
- expect(result.includes(preset.droppables[id])).toBe(true);
+ expect(result).toContain(preset.draggables[id]);
});
});
- it('should request all the droppables to start listening to scroll events', () => {
- const callbacks = getCallbackStub();
- const marshal = createDimensionMarshal(callbacks);
- const watchers = populateMarshal(marshal);
-
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- // initial droppable
- expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1);
- // clearing this initial call
- watchers.droppable.watchScroll.mockClear();
-
- marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
- requestAnimationFrame.step(2);
-
- // excluding the home droppable
- const expectedLength: number = Object.keys(preset.droppables).length - 1;
- expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(expectedLength);
+ [scheduled, immediate].forEach((scrollOptions: ScrollOptions) => {
+ it('should request all the droppables to start listening to scroll events', () => {
+ const callbacks = getCallbackStub();
+ const marshal = createDimensionMarshal(callbacks);
+ const watchers = populateMarshal(marshal);
+
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions,
+ }));
+ // initial droppable
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1);
+
+ marshal.onPhaseChange(withScrollOptions(
+ state.dragging(preset.inHome1.descriptor.id),
+ scrollOptions,
+ ));
+ requestAnimationFrame.step(2);
+
+ const expectedLength: number = Object.keys(preset.droppables).length;
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(expectedLength);
+
+ Object.keys(preset.droppables).forEach((id: DroppableId) => {
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id, scrollOptions);
+ });
+ });
});
it('should not publish dimensions that where not collected', () => {
@@ -501,12 +605,20 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal, { draggables, droppables });
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
- expect(callbacks.publishDroppables.mock.calls[0][0]).not.toContain(ofAnotherType);
- expect(callbacks.publishDraggables.mock.calls[0][0]).not.toContain(childOfAnotherType);
+ expect(callbacks.bulkPublish.mock.calls[0][0]).not.toContain(ofAnotherType);
+ // validation
+ expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.foreign);
+
+ expect(callbacks.bulkPublish.mock.calls[0][1]).not.toContain(childOfAnotherType);
+ // validation
+ expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.inHome2);
});
it('should not publish draggables if there are none to publish', () => {
@@ -521,19 +633,19 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal, { draggables, droppables });
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// asserting initial lift occurred
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]);
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]);
- callbacks.publishDraggables.mockReset();
- callbacks.publishDroppables.mockReset();
+ expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1);
+ expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home);
// perform full lift
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
- expect(callbacks.publishDraggables).not.toHaveBeenCalled();
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.foreign]);
+ expect(callbacks.bulkPublish).toHaveBeenCalledWith([preset.foreign], []);
});
it('should not publish droppables if there are none to publish', () => {
@@ -551,19 +663,19 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal, { draggables, droppables });
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// asserting initial lift occurred
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]);
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]);
- callbacks.publishDraggables.mockReset();
- callbacks.publishDroppables.mockReset();
+ expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1);
+ expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home);
// perform full lift
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
- expect(callbacks.publishDroppables).not.toHaveBeenCalled();
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome2]);
+ expect(callbacks.bulkPublish).toHaveBeenCalledWith([], [preset.inHome2]);
});
});
});
@@ -580,13 +692,16 @@ describe('dimension marshal', () => {
const watchers = populateMarshal(marshal);
// do initial work
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
// currently only watching
Object.keys(preset.droppables).forEach((id: DroppableId) => {
- expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id);
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id, scheduled);
expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalledWith(id);
});
@@ -608,8 +723,9 @@ describe('dimension marshal', () => {
let watchers;
const resetMocks = () => {
- callbacks.publishDraggables.mockClear();
- callbacks.publishDroppables.mockClear();
+ callbacks.publishDraggable.mockClear();
+ callbacks.publishDroppable.mockClear();
+ callbacks.bulkPublish.mockClear();
watchers.draggable.getDimension.mockClear();
watchers.droppable.getDimension.mockClear();
watchers.droppable.watchScroll.mockClear();
@@ -622,107 +738,129 @@ describe('dimension marshal', () => {
watchers = populateMarshal(marshal);
});
- const shouldHaveProcessedInitialDimensions = (): void => {
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]);
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]);
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
+ const shouldHaveProcessedInitialDimensions = (scrollOptions: ScrollOptions): void => {
+ expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home);
+ expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1);
+ expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1);
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(1);
expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(1);
- expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(preset.home.descriptor.id);
expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1);
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, scrollOptions
+ );
expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled();
};
const shouldNotHavePublishedDimensions = (): void => {
- expect(callbacks.publishDroppables).not.toHaveBeenCalled();
- expect(callbacks.publishDroppables).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
+ expect(callbacks.bulkPublish).not.toHaveBeenCalled();
};
- it('should support subsequent drags after a completed collection', () => {
- Array.from({ length: 4 }).forEach(() => {
- // initial publish
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
-
- shouldHaveProcessedInitialDimensions();
-
- // resetting mock state so future assertions do not include these calls
- resetMocks();
-
- // collection and publish of secondary dimensions
- marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
- requestAnimationFrame.step(2);
+ [immediate, scheduled].forEach((scrollOptions: ScrollOptions) => {
+ it('should support subsequent drags after a completed collection', () => {
+ Array.from({ length: 4 }).forEach(() => {
+ // initial publish
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions,
+ }));
+
+ shouldHaveProcessedInitialDimensions(scrollOptions);
+
+ // resetting mock state so future assertions do not include these calls
+ resetMocks();
+
+ // collection and publish of secondary dimensions
+ marshal.onPhaseChange(withScrollOptions(
+ state.dragging(preset.inHome1.descriptor.id),
+ scrollOptions,
+ ));
+ requestAnimationFrame.step(2);
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
- expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(droppableCount - 1);
- expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(droppableCount - 1);
- expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(draggableCount - 1);
- expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
+ expect(callbacks.publishDraggable).not.toHaveBeenCalled();
+ expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1);
+ expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(droppableCount - 1);
+ expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(droppableCount - 1);
+ expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(draggableCount - 1);
+ expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled();
- // finish the collection
- marshal.onPhaseChange(state.dropComplete(preset.inHome1.descriptor.id));
+ // finish the collection
+ marshal.onPhaseChange(state.dropComplete(preset.inHome1.descriptor.id));
- expect(watchers.droppable.unwatchScroll).toHaveBeenCalledTimes(droppableCount);
+ expect(watchers.droppable.unwatchScroll).toHaveBeenCalledTimes(droppableCount);
- resetMocks();
+ resetMocks();
+ });
});
- });
- it('should support subsequent drags after a cancelled dimension request', () => {
- Array.from({ length: 4 }).forEach(() => {
- // start the collection
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ it('should support subsequent drags after a cancelled dimension request', () => {
+ Array.from({ length: 4 }).forEach(() => {
+ // start the collection
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions,
+ }));
- shouldHaveProcessedInitialDimensions();
- resetMocks();
+ shouldHaveProcessedInitialDimensions(scrollOptions);
+ resetMocks();
- // cancelled
- marshal.onPhaseChange(state.idle);
+ // cancelled
+ marshal.onPhaseChange(state.idle);
- shouldNotHavePublishedDimensions();
+ shouldNotHavePublishedDimensions();
- resetMocks();
+ resetMocks();
+ });
});
- });
- it('should support subsequent drags after a cancelled drag', () => {
- Array.from({ length: 4 }).forEach(() => {
- // start the collection
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ it('should support subsequent drags after a cancelled drag', () => {
+ Array.from({ length: 4 }).forEach(() => {
+ // start the collection
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions,
+ }));
- shouldHaveProcessedInitialDimensions();
- resetMocks();
+ shouldHaveProcessedInitialDimensions(scrollOptions);
+ resetMocks();
- // drag started but collection not started
- marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
+ // drag started but collection not started
+ marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
- shouldNotHavePublishedDimensions();
+ shouldNotHavePublishedDimensions();
- // cancelled
- marshal.onPhaseChange(state.idle);
- resetMocks();
+ // cancelled
+ marshal.onPhaseChange(state.idle);
+ resetMocks();
+ });
});
- });
- it('should support subsequent drags after a cancelled collection', () => {
- Array.from({ length: 4 }).forEach(() => {
- // start the collection
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ it('should support subsequent drags after a cancelled collection', () => {
+ Array.from({ length: 4 }).forEach(() => {
+ // start the collection
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions,
+ }));
- shouldHaveProcessedInitialDimensions();
- resetMocks();
+ shouldHaveProcessedInitialDimensions(scrollOptions);
+ resetMocks();
- // drag started but collection not started
- marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
- // executing collection step but not publish
- requestAnimationFrame.step();
+ // drag started but collection not started
+ marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
+ // executing collection step but not publish
+ requestAnimationFrame.step();
- shouldNotHavePublishedDimensions();
+ shouldNotHavePublishedDimensions();
- // cancelled
- marshal.onPhaseChange(state.idle);
- resetMocks();
+ // cancelled
+ marshal.onPhaseChange(state.idle);
+ resetMocks();
+ });
});
});
});
@@ -732,6 +870,7 @@ describe('dimension marshal', () => {
getDimension: () => preset.home,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => {},
};
const getDraggableDimensionFn: GetDraggableDimensionFn = () => preset.inHome1;
@@ -746,9 +885,12 @@ describe('dimension marshal', () => {
marshal.registerDroppable(preset.home.descriptor, droppableCallbacks);
marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]);
+ expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home);
});
it('should overwrite an existing entry if needed', () => {
@@ -759,7 +901,7 @@ describe('dimension marshal', () => {
});
marshal.registerDroppable(preset.home.descriptor, droppableCallbacks);
- const newDescriptor: DroppableDescriptor = {
+ const newHomeDescriptor: DroppableDescriptor = {
id: preset.home.descriptor.id,
type: preset.home.descriptor.type,
};
@@ -767,13 +909,17 @@ describe('dimension marshal', () => {
getDimension: () => preset.foreign,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => { },
};
- marshal.registerDroppable(newDescriptor, newCallbacks);
+ marshal.registerDroppable(newHomeDescriptor, newCallbacks);
marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
- expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.foreign]);
- expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.foreign);
+ expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1);
});
});
@@ -794,9 +940,12 @@ describe('dimension marshal', () => {
marshal.registerDroppable(preset.home.descriptor, droppableCallbacks);
marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]);
+ expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1);
});
it('should overwrite an existing entry if needed', () => {
@@ -817,10 +966,13 @@ describe('dimension marshal', () => {
index: preset.inHome1.descriptor.index + 10,
};
marshal.registerDraggable(fake, () => preset.inHome2);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
- expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1);
- expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome2]);
+ expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1);
+ expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome2);
});
});
});
@@ -862,9 +1014,10 @@ describe('dimension marshal', () => {
expect(console.warn).not.toHaveBeenCalled();
// lift, collect and publish
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- // clearing state from original publish
- callbacks.publishDroppables.mockClear();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// execute full lift
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
@@ -872,8 +1025,10 @@ describe('dimension marshal', () => {
expect(watchers.droppable.getDimension)
.not.toHaveBeenCalledWith(preset.foreign.descriptor.id);
- expect(callbacks.publishDroppables.mock.calls[0][0])
+ expect(callbacks.bulkPublish.mock.calls[0][0])
.not.toContain(preset.foreign);
+ // validation
+ expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.emptyForeign);
// checking we are not causing an orphan child warning
expect(console.warn).not.toHaveBeenCalled();
@@ -891,7 +1046,10 @@ describe('dimension marshal', () => {
// not unregistering children (bad)
// lift, collect and publish
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// perform full lift
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
@@ -916,6 +1074,7 @@ describe('dimension marshal', () => {
getDimension: getOldDimension,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => { },
});
marshal.registerDraggable(preset.inHome1.descriptor, () => preset.inHome1);
@@ -934,10 +1093,14 @@ describe('dimension marshal', () => {
getDimension: getNewDimension,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => { },
});
// perform full lift
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
@@ -962,7 +1125,10 @@ describe('dimension marshal', () => {
marshal.unregisterDraggable(preset.inForeign1.descriptor);
expect(console.error).not.toHaveBeenCalled();
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
// perform full lift
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
@@ -977,6 +1143,7 @@ describe('dimension marshal', () => {
getDimension: () => preset.home,
watchScroll: () => {},
unwatchScroll: () => {},
+ scroll: () => {},
});
const getOldDimension: GetDraggableDimensionFn =
jest.fn().mockImplementation(() => preset.inHome2);
@@ -1005,7 +1172,10 @@ describe('dimension marshal', () => {
marshal.unregisterDraggable(preset.inHome2.descriptor);
// perform full lift
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id));
requestAnimationFrame.step(2);
@@ -1034,12 +1204,15 @@ describe('dimension marshal', () => {
});
// start a collection
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- callbacks.publishDraggables.mockReset();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
+ callbacks.publishDraggable.mockReset();
// now registering
marshal.registerDraggable(fake.descriptor, () => fake);
- expect(callbacks.publishDraggables).not.toHaveBeenCalled();
+ expect(callbacks.publishDraggable).not.toHaveBeenCalled();
expect(console.warn).toHaveBeenCalled();
});
});
@@ -1060,18 +1233,22 @@ describe('dimension marshal', () => {
getDimension: () => fake,
watchScroll: jest.fn(),
unwatchScroll: () => { },
+ scroll: () => {},
};
// starting collection
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
- callbacks.publishDroppables.mockReset();
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
+ callbacks.publishDroppable.mockReset();
// updating registration
marshal.registerDroppable(fake.descriptor, droppableCallbacks);
// warning should have been logged and nothing updated
expect(console.warn).toHaveBeenCalled();
- expect(callbacks.publishDroppables).not.toHaveBeenCalled();
+ expect(callbacks.publishDroppable).not.toHaveBeenCalled();
expect(droppableCallbacks.watchScroll).not.toHaveBeenCalled();
});
});
@@ -1105,7 +1282,7 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting());
marshal.updateDroppableScroll(preset.home.descriptor.id, { x: 100, y: 230 });
expect(callbacks.updateDroppableScroll)
@@ -1142,7 +1319,10 @@ describe('dimension marshal', () => {
const marshal = createDimensionMarshal(callbacks);
populateMarshal(marshal);
- marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id));
+ marshal.onPhaseChange(state.requesting({
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: scheduled,
+ }));
marshal.updateDroppableIsEnabled(preset.home.descriptor.id, false);
expect(callbacks.updateDroppableIsEnabled)
diff --git a/test/unit/view/drag-drop-context.spec.js b/test/unit/view/drag-drop-context.spec.js
index 49551c9338..5c96a698aa 100644
--- a/test/unit/view/drag-drop-context.spec.js
+++ b/test/unit/view/drag-drop-context.spec.js
@@ -105,7 +105,7 @@ describe('DragDropContext', () => {
{ }}>
{(droppableProvided: DroppableProvided) => (
-
+
{(draggableProvided: DraggableProvided) => (
@@ -141,7 +141,7 @@ describe('DragDropContext', () => {
{ }}>
{(droppableProvided: DroppableProvided) => (
-
+
{(draggableProvided: DraggableProvided) => (
diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js
index 3a3bf2b947..5eaa5215ad 100644
--- a/test/unit/view/drag-handle.spec.js
+++ b/test/unit/view/drag-handle.spec.js
@@ -18,12 +18,12 @@ import {
} from '../../utils/user-input-util';
import type { Position, DraggableId } from '../../../src/types';
import * as keyCodes from '../../../src/view/key-codes';
-import getWindowScrollPosition from '../../../src/view/get-window-scroll-position';
+import getWindowScroll from '../../../src/window/get-window-scroll';
import setWindowScroll from '../../utils/set-window-scroll';
-import forceUpdate from '../../utils/force-update';
import getArea from '../../../src/state/get-area';
import { timeForLongPress, forcePressThreshold } from '../../../src/view/drag-handle/sensor/create-touch-sensor';
import { interactiveTagNames } from '../../../src/view/drag-handle/util/should-allow-dragging-from-target';
+import type { TagNameMap } from '../../../src/view/drag-handle/util/should-allow-dragging-from-target';
import { styleContextKey, canLiftContextKey } from '../../../src/view/context-keys';
const primaryButton: number = 0;
@@ -220,6 +220,32 @@ describe('drag handle', () => {
expect(myMock.mock.calls[0][0]['data-react-beautiful-dnd-drag-handle']).toEqual(basicContext[styleContextKey]);
});
+ it('should apply a default aria roledescription containing lift instructions', () => {
+ const myMock = jest.fn();
+ myMock.mockReturnValue(
hello world
);
+
+ mount(
+
fakeDraggableRef}
+ canDragInteractiveElements={false}
+ >
+ {(dragHandleProps: ?DragHandleProps) => (
+ myMock(dragHandleProps)
+ )}
+ ,
+ { context: basicContext }
+ );
+
+ // $ExpectError - using lots of accessors
+ expect(myMock.mock.calls[0][0]['aria-roledescription'])
+ .toBe('Draggable item. Press space bar to lift');
+ });
+
describe('mouse dragging', () => {
describe('initiation', () => {
it('should start a drag if there was sufficient mouse movement in any direction', () => {
@@ -253,7 +279,7 @@ describe('drag handle', () => {
windowMouseMove(point);
expect(customCallbacks.onLift)
- .toHaveBeenCalledWith({ client: point, isScrollAllowed: true });
+ .toHaveBeenCalledWith({ client: point, autoScrollMode: 'FLUID' });
customWrapper.unmount();
});
@@ -515,7 +541,7 @@ describe('drag handle', () => {
});
describe('window scroll during drag', () => {
- const originalScroll: Position = getWindowScrollPosition();
+ const originalScroll: Position = getWindowScroll();
beforeEach(() => {
setWindowScroll(origin, { shouldPublish: false });
@@ -1166,7 +1192,7 @@ describe('drag handle', () => {
expect(callbacks.onLift).toHaveBeenCalledWith({
client: fakeCenter,
- isScrollAllowed: false,
+ autoScrollMode: 'JUMP',
});
});
@@ -1263,6 +1289,40 @@ describe('drag handle', () => {
})).toBe(true);
});
+ it('should instantly fire a scroll action when the window scrolls', () => {
+ // lift
+ pressSpacebar(wrapper);
+ // scroll event
+ window.dispatchEvent(new Event('scroll'));
+
+ expect(callbacksCalled(callbacks)({
+ onLift: 1,
+ onWindowScroll: 1,
+ })).toBe(true);
+ });
+
+ it('should prevent using keyboard keys that modify scroll', () => {
+ const keys: number[] = [
+ keyCodes.pageUp,
+ keyCodes.pageDown,
+ keyCodes.home,
+ keyCodes.end,
+ ];
+
+ // lift
+ pressSpacebar(wrapper);
+
+ keys.forEach((keyCode: number) => {
+ const mockEvent: MockEvent = createMockEvent();
+ const trigger = withKeyboard(keyCode);
+
+ trigger(wrapper, mockEvent);
+
+ expect(wasEventStopped(mockEvent)).toBe(true);
+ expect(callbacks.onWindowScroll).not.toHaveBeenCalled();
+ });
+ });
+
it('should stop dragging if the keyboard is used after a lift and a direction is not provided', () => {
const customCallbacks = getStubCallbacks();
const customWrapper = mount(
@@ -1557,28 +1617,8 @@ describe('drag handle', () => {
});
});
- it('should cancel when the window is resized', () => {
- // lift
- pressSpacebar(wrapper);
- // resize event
- window.dispatchEvent(new Event('resize'));
-
- expect(callbacksCalled(callbacks)({
- onLift: 1,
- onCancel: 1,
- })).toBe(true);
- });
-
- it('should cancel if the window is scrolled', () => {
- // lift
- pressSpacebar(wrapper);
- // scroll event
- window.dispatchEvent(new Event('scroll'));
-
- expect(callbacksCalled(callbacks)({
- onLift: 1,
- onCancel: 1,
- })).toBe(true);
+ it.skip('should cancel on a page visibility change', () => {
+ // TODO
});
it('should not do anything if there is nothing dragging', () => {
@@ -1737,7 +1777,10 @@ describe('drag handle', () => {
touchStart(wrapper, client);
jest.runTimersToTime(timeForLongPress);
- expect(callbacks.onLift).toHaveBeenCalledWith({ client, isScrollAllowed: false });
+ expect(callbacks.onLift).toHaveBeenCalledWith({
+ client,
+ autoScrollMode: 'FLUID',
+ });
});
it('should not fire a second lift after movement that would have otherwise have started a drag', () => {
@@ -1935,6 +1978,25 @@ describe('drag handle', () => {
expect(event.defaultPrevented).toBe(true);
});
+
+ it('should schedule a window scroll move on window scroll', () => {
+ start();
+
+ dispatchWindowEvent('scroll');
+ dispatchWindowEvent('scroll');
+ dispatchWindowEvent('scroll');
+
+ // not called initially
+ expect(callbacks.onWindowScroll).not.toHaveBeenCalled();
+
+ // called after a requestAnimationFrame
+ requestAnimationFrame.step();
+ expect(callbacks.onWindowScroll).toHaveBeenCalledTimes(1);
+
+ // should not add any additional calls
+ requestAnimationFrame.flush();
+ expect(callbacks.onWindowScroll).toHaveBeenCalledTimes(1);
+ });
});
describe('dropping', () => {
@@ -2052,15 +2114,6 @@ describe('drag handle', () => {
})).toBe(true);
});
- it('should cancel the drag after a window scroll', () => {
- dispatchWindowEvent('scroll');
-
- expect(callbacksCalled(callbacks)({
- onLift: 1,
- onCancel: 1,
- })).toBe(true);
- });
-
it('should cancel a drag if any keypress is made', () => {
// end initial drag
end();
@@ -2366,9 +2419,6 @@ describe('drag handle', () => {
const controls: Control[] = [mouse, keyboard, touch];
- const getAria = (wrap?: ReactWrapper = wrapper): boolean =>
- Boolean(wrap.find(Child).props().dragHandleProps['aria-grabbed']);
-
beforeEach(() => {
jest.useFakeTimers();
});
@@ -2379,37 +2429,6 @@ describe('drag handle', () => {
controls.forEach((control: Control) => {
describe(`control: ${control.name}`, () => {
- describe('aria', () => {
- it('should not set the aria attribute of dragging if not dragging', () => {
- expect(getAria()).toBe(false);
- });
-
- it('should not set the aria attribute of dragging if a drag is pending', () => {
- control.preLift();
- forceUpdate(wrapper);
-
- expect(getAria()).toBe(false);
- });
-
- it('should set the aria attribute of dragging if a drag is occurring', () => {
- control.preLift();
- control.lift();
- forceUpdate(wrapper);
-
- expect(getAria()).toBe(true);
- });
-
- it('should set the aria attribute if drag is finished', () => {
- control.preLift();
- control.lift();
- forceUpdate(wrapper);
- control.end();
- forceUpdate(wrapper);
-
- expect(getAria()).toBe(false);
- });
- });
-
describe('window bindings', () => {
it('should unbind all window listeners when drag ends', () => {
jest.spyOn(window, 'addEventListener');
@@ -2441,9 +2460,9 @@ describe('drag handle', () => {
});
describe('interactive element interactions', () => {
- const mixedCase = (items: string[]): string[] => [
- ...items.map((i: string): string => i.toLowerCase()),
- ...items.map((i: string): string => i.toUpperCase()),
+ const mixedCase = (map: TagNameMap): string[] => [
+ ...Object.keys(map).map((tagName: string) => tagName.toLowerCase()),
+ ...Object.keys(map).map((tagName: string) => tagName.toUpperCase()),
];
it('should not start a drag if the target is an interactive element', () => {
@@ -2484,9 +2503,12 @@ describe('drag handle', () => {
});
it('should start a drag if the target is not an interactive element', () => {
- const nonInteractiveTagNames: string[] = [
- 'a', 'div', 'span', 'header',
- ];
+ const nonInteractiveTagNames: TagNameMap = {
+ a: true,
+ div: true,
+ span: true,
+ header: true,
+ };
// counting call count between loops
let count: number = 0;
diff --git a/test/unit/view/draggable-dimension-publisher.spec.js b/test/unit/view/draggable-dimension-publisher.spec.js
index 0bde413244..b1f06985e0 100644
--- a/test/unit/view/draggable-dimension-publisher.spec.js
+++ b/test/unit/view/draggable-dimension-publisher.spec.js
@@ -77,6 +77,7 @@ const getMarshalStub = (): DimensionMarshal => ({
unregisterDroppable: jest.fn(),
updateDroppableScroll: jest.fn(),
updateDroppableIsEnabled: jest.fn(),
+ scrollDroppable: jest.fn(),
onPhaseChange: jest.fn(),
});
diff --git a/test/unit/view/droppable-dimension-publisher.spec.js b/test/unit/view/droppable-dimension-publisher.spec.js
index a54ddccf83..f55134c8ba 100644
--- a/test/unit/view/droppable-dimension-publisher.spec.js
+++ b/test/unit/view/droppable-dimension-publisher.spec.js
@@ -15,6 +15,7 @@ import type {
} from '../../../src/state/dimension-marshal/dimension-marshal-types';
import type {
Area,
+ ScrollOptions,
Spacing,
DroppableId,
DroppableDimension,
@@ -89,6 +90,86 @@ class ScrollableItem extends Component
}
}
+type AppProps = {
+ droppableIsScrollable?: boolean,
+ parentIsScrollable?: boolean,
+ ignoreContainerClipping: boolean,
+};
+type AppState = {
+ ref: ?HTMLElement,
+}
+
+const frame: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 150,
+ bottom: 150,
+});
+const client: Area = getArea({
+ top: 0,
+ left: 0,
+ right: 100,
+ bottom: 100,
+});
+const descriptor: DroppableDescriptor = {
+ id: 'a cool droppable',
+ type: 'cool',
+};
+
+class App extends Component {
+ static defaultProps = {
+ onPublish: () => {},
+ ignoreContainerClipping: false,
+ }
+
+ state = { ref: null }
+ setRef = ref => this.setState({ ref })
+ render() {
+ const {
+ droppableIsScrollable,
+ parentIsScrollable,
+ ignoreContainerClipping,
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
const getMarshalStub = (): DimensionMarshal => ({
registerDraggable: jest.fn(),
unregisterDraggable: jest.fn(),
@@ -97,8 +178,16 @@ const getMarshalStub = (): DimensionMarshal => ({
updateDroppableScroll: jest.fn(),
updateDroppableIsEnabled: jest.fn(),
onPhaseChange: jest.fn(),
+ scrollDroppable: jest.fn(),
});
+const scheduled: ScrollOptions = {
+ shouldPublishImmediately: false,
+};
+const immediate: ScrollOptions = {
+ shouldPublishImmediately: true,
+};
+
describe('DraggableDimensionPublisher', () => {
const originalWindowScroll: Position = {
x: window.pageXOffset,
@@ -330,7 +419,7 @@ describe('DraggableDimensionPublisher', () => {
y: 1000,
};
setWindowScroll(windowScroll, { shouldPublish: false });
- const client: Area = getArea({
+ const ourClient: Area = getArea({
top: 0,
right: 100,
bottom: 100,
@@ -342,9 +431,9 @@ describe('DraggableDimensionPublisher', () => {
type: 'fake',
},
windowScroll,
- client,
+ client: ourClient,
});
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => client);
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ourClient);
jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing);
mount(
@@ -363,149 +452,138 @@ describe('DraggableDimensionPublisher', () => {
expect(result).toEqual(expected);
});
- it('should capture the initial scroll containers current scroll', () => {
- const marshal: DimensionMarshal = getMarshalStub();
- const frameScroll: Position = {
- x: 500,
- y: 1000,
- };
- const expected: DroppableDimension = getDroppableDimension({
- descriptor: {
- id: 'my-fake-id',
- type: 'fake',
- },
- client: getArea({
- top: 0,
- right: 100,
- bottom: 200,
- left: 0,
- }),
- frameScroll,
+ describe('closest scrollable', () => {
+ describe('no closest scrollable', () => {
+ it('should return null for the closest scrollable if there is no scroll container', () => {
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client,
+ });
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const droppableNode = wrapper.state().ref;
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
+
+ // pull the get dimension function out
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // execute it to get the dimension
+ const result: DroppableDimension = callbacks.getDimension();
+
+ expect(result).toEqual(expected);
+ });
});
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- top: expected.page.withoutMargin.top,
- bottom: expected.page.withoutMargin.bottom,
- left: expected.page.withoutMargin.left,
- right: expected.page.withoutMargin.right,
- height: expected.page.withoutMargin.height,
- width: expected.page.withoutMargin.width,
- }));
- jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({
- overflow: 'auto',
- ...noSpacing,
- }));
- const wrapper = mount(
- ,
- withDimensionMarshal(marshal)
- );
- // setting initial scroll
- const container: HTMLElement = wrapper.getDOMNode();
- container.scrollLeft = frameScroll.x;
- container.scrollTop = frameScroll.y;
- // pull the get dimension function out
- const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- // execute it to get the dimension
- const result: DroppableDimension = callbacks.getDimension();
+ describe('droppable is scrollable', () => {
+ it('should capture the frame', () => {
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client: frame,
+ closest: {
+ frameClient: frame,
+ scrollWidth: frame.width,
+ scrollHeight: frame.height,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+ const marshal: DimensionMarshal = getMarshalStub();
+ // both the droppable and the parent are scrollable
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const droppableNode = wrapper.state().ref;
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => frame);
- expect(result).toEqual(expected);
- });
+ // pull the get dimension function out
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // execute it to get the dimension
+ const result: DroppableDimension = callbacks.getDimension();
- describe('calculating the frame', () => {
- const frame: Area = getArea({
- top: 0,
- left: 0,
- right: 150,
- bottom: 150,
- });
- const client: Area = getArea({
- top: 0,
- left: 0,
- right: 100,
- bottom: 100,
+ expect(result).toEqual(expected);
+ });
});
- const descriptor: DroppableDescriptor = {
- id: 'a cool droppable',
- type: 'cool',
- };
- const dimensionWithoutScrollParent: DroppableDimension = getDroppableDimension({
- descriptor,
- client,
- });
- const dimensionWithScrollParent: DroppableDimension = getDroppableDimension({
- descriptor,
- client,
- frameClient: frame,
+ describe('parent of droppable is scrollable', () => {
+ it('should capture the frame', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const parentNode = wrapper.getDOMNode();
+ const droppableNode = wrapper.state().ref;
+ jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client,
+ closest: {
+ frameClient: frame,
+ scrollWidth: frame.width,
+ scrollHeight: frame.height,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ // pull the get dimension function out
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // execute it to get the dimension
+ const result: DroppableDimension = callbacks.getDimension();
+
+ expect(result).toEqual(expected);
+ });
});
- type AppProps = {
- droppableIsScrollable?: boolean,
- parentIsScrollable?: boolean,
- ignoreContainerClipping: boolean,
- };
- type AppState = {
- ref: ?HTMLElement,
- }
-
- class App extends Component {
- static defaultProps = {
- onPublish: () => {},
- ignoreContainerClipping: false,
- }
-
- state = { ref: null }
- setRef = ref => this.setState({ ref })
- render() {
- const {
- droppableIsScrollable,
- parentIsScrollable,
- ignoreContainerClipping,
- } = this.props;
- return (
-
+ describe('both droppable and parent is scrollable', () => {
+ it('should only consider the closest scrollable - which is the droppable', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
);
- }
- }
+ const parentNode = wrapper.getDOMNode();
+ const droppableNode = wrapper.state().ref;
+ jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client,
+ closest: {
+ frameClient: client,
+ scrollWidth: client.width,
+ scrollHeight: client.height,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+
+ // pull the get dimension function out
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // execute it to get the dimension
+ const result: DroppableDimension = callbacks.getDimension();
+
+ expect(result).toEqual(expected);
+ });
+ });
- it('should detect a scrollable parent', () => {
+ it('should capture the initial scroll of the scrollest scrollable', () => {
+ // in this case the parent of the droppable is the closest scrollable
+ const frameScroll: Position = { x: 10, y: 20 };
const marshal: DimensionMarshal = getMarshalStub();
const wrapper = mount(
{
/>,
withDimensionMarshal(marshal),
);
- const parentNode = wrapper.getDOMNode();
const droppableNode = wrapper.state().ref;
+ const parentNode = wrapper.getDOMNode();
+ // manually setting the scroll of the parent node
+ parentNode.scrollTop = frameScroll.y;
+ parentNode.scrollLeft = frameScroll.x;
jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
-
- // pull the get dimension function out
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client,
+ closest: {
+ frameClient: frame,
+ scrollWidth: frame.width,
+ scrollHeight: frame.height,
+ scroll: frameScroll,
+ shouldClipSubject: true,
+ },
+ });
+
+ // pull the get dimension function out
const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
// execute it to get the dimension
const result: DroppableDimension = callbacks.getDimension();
- expect(result).toEqual(dimensionWithScrollParent);
+ expect(result).toEqual(expected);
});
- it('should ignore any parents if they are not scroll containers', () => {
+ it('should indicate if subject clipping is permitted based on the ignoreContainerClipping prop', () => {
+ // in this case the parent of the droppable is the closest scrollable
const marshal: DimensionMarshal = getMarshalStub();
const wrapper = mount(
,
withDimensionMarshal(marshal),
);
- const parentNode = wrapper.getDOMNode();
const droppableNode = wrapper.state().ref;
+ const parentNode = wrapper.getDOMNode();
jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
-
- // pull the get dimension function out
+ const expected: DroppableDimension = getDroppableDimension({
+ descriptor,
+ client,
+ closest: {
+ frameClient: frame,
+ scrollWidth: frame.width,
+ scrollHeight: frame.height,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: false,
+ },
+ });
+
+ // pull the get dimension function out
const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
// execute it to get the dimension
const result: DroppableDimension = callbacks.getDimension();
- expect(result).toEqual(dimensionWithoutScrollParent);
+ expect(result).toEqual(expected);
});
+ });
+ });
- it('should use itself as the frame if the droppable is scrollable', () => {
+ describe('scroll watching', () => {
+ const scroll = (el: HTMLElement, target: Position) => {
+ el.scrollTop = target.y;
+ el.scrollLeft = target.x;
+ el.dispatchEvent(new Event('scroll'));
+ };
+
+ describe('should immediately publish updates', () => {
+ it('should immediately publish the scroll offset of the closest scrollable', () => {
const marshal: DimensionMarshal = getMarshalStub();
- // both the droppable and the parent are scrollable
const wrapper = mount(
- ,
+ ,
withDimensionMarshal(marshal),
);
- const parentNode = wrapper.getDOMNode();
- const droppableNode = wrapper.state().ref;
- jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
- jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
+ const container: HTMLElement = wrapper.getDOMNode();
+
+ if (!container.classList.contains('scroll-container')) {
+ throw new Error('incorrect dom node collected');
+ }
- // pull the get dimension function out
+ // tell the droppable to watch for scrolling
const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- // execute it to get the dimension
- const result: DroppableDimension = callbacks.getDimension();
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(immediate);
- expect(result).toEqual(dimensionWithoutScrollParent);
+ scroll(container, { x: 500, y: 1000 });
+
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 500, y: 1000 },
+ );
});
- it('should return ignore the parent frame when ignoreContainerClipping is set', () => {
+ it('should not fire a scroll if the value has not changed since the previous call', () => {
+ // this can happen if you scroll backward and forward super quick
const marshal: DimensionMarshal = getMarshalStub();
const wrapper = mount(
- ,
+ ,
withDimensionMarshal(marshal),
);
- const parentNode = wrapper.getDOMNode();
- const droppableNode = wrapper.state().ref;
- jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
- jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
-
- // pull the get dimension function out
+ const container: HTMLElement = wrapper.getDOMNode();
+ // tell the droppable to watch for scrolling
const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- // execute it to get the dimension
- const result: DroppableDimension = callbacks.getDimension();
- expect(result).toEqual(dimensionWithoutScrollParent);
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(immediate);
+
+ // first event
+ scroll(container, { x: 500, y: 1000 });
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 500, y: 1000 }
+ );
+ marshal.updateDroppableScroll.mockReset();
+
+ // second event - scroll to same spot
+ scroll(container, { x: 500, y: 1000 });
+ expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
+
+ // third event - new value
+ scroll(container, { x: 500, y: 1001 });
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 500, y: 1001 }
+ );
});
});
- });
- describe('scroll watching', () => {
- const scroll = (el: HTMLElement, target: Position) => {
- el.scrollTop = target.y;
- el.scrollLeft = target.x;
- el.dispatchEvent(new Event('scroll'));
- };
+ describe('should schedule publish updates', () => {
+ it('should publish the scroll offset of the closest scrollable', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
- it('should publish the scroll offset of the closest scrollable', () => {
- const marshal: DimensionMarshal = getMarshalStub();
- const wrapper = mount(
- ,
- withDimensionMarshal(marshal),
- );
- const container: HTMLElement = wrapper.getDOMNode();
+ if (!container.classList.contains('scroll-container')) {
+ throw new Error('incorrect dom node collected');
+ }
- if (!container.classList.contains('scroll-container')) {
- throw new Error('incorrect dom node collected');
- }
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(scheduled);
- // tell the droppable to watch for scrolling
- const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- // watch scroll will only be called after the dimension is requested
- callbacks.getDimension();
- callbacks.watchScroll();
+ scroll(container, { x: 500, y: 1000 });
+ // release the update animation frame
+ requestAnimationFrame.step();
- scroll(container, { x: 500, y: 1000 });
- // release the update animation frame
- requestAnimationFrame.step();
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 500, y: 1000 },
+ );
+ });
- expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
- preset.home.descriptor.id, { x: 500, y: 1000 },
- );
- });
+ it('should throttle multiple scrolls into a animation frame', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- it('should throttle multiple scrolls into a animation frame', () => {
- const marshal: DimensionMarshal = getMarshalStub();
- const wrapper = mount(
- ,
- withDimensionMarshal(marshal),
- );
- const container: HTMLElement = wrapper.getDOMNode();
- // tell the droppable to watch for scrolling
- const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(scheduled);
- // watch scroll will only be called after the dimension is requested
- callbacks.getDimension();
- callbacks.watchScroll();
+ // first event
+ scroll(container, { x: 500, y: 1000 });
+ // second event in same frame
+ scroll(container, { x: 200, y: 800 });
- // first event
- scroll(container, { x: 500, y: 1000 });
- // second event in same frame
- scroll(container, { x: 200, y: 800 });
+ // release the update animation frame
+ requestAnimationFrame.step();
- // release the update animation frame
- requestAnimationFrame.step();
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 200, y: 800 },
+ );
- expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
- expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
- preset.home.descriptor.id, { x: 200, y: 800 },
- );
+ // also checking that no loose frames are stored up
+ requestAnimationFrame.flush();
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
+ });
- // also checking that no loose frames are stored up
- requestAnimationFrame.flush();
- expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
- });
+ 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 marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- 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 marshal: DimensionMarshal = getMarshalStub();
- const wrapper = mount(
- ,
- withDimensionMarshal(marshal),
- );
- const container: HTMLElement = wrapper.getDOMNode();
- // tell the droppable to watch for scrolling
- const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(scheduled);
+
+ // first event
+ scroll(container, { x: 500, y: 1000 });
+ // release the frame
+ requestAnimationFrame.step();
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
+ preset.home.descriptor.id, { x: 500, y: 1000 }
+ );
+ marshal.updateDroppableScroll.mockReset();
- // watch scroll will only be called after the dimension is requested
- callbacks.getDimension();
- callbacks.watchScroll();
+ // second event
+ scroll(container, { x: 501, y: 1001 });
+ // no frame to release change yet
- // first event
- scroll(container, { x: 500, y: 1000 });
- // release the frame
- requestAnimationFrame.step();
- expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
- expect(marshal.updateDroppableScroll).toHaveBeenCalledWith(
- preset.home.descriptor.id, { x: 500, y: 1000 }
- );
- marshal.updateDroppableScroll.mockReset();
+ // third event - back to original value
+ scroll(container, { x: 500, y: 1000 });
+ // release the frame
+ requestAnimationFrame.step();
+ expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
+ });
- // second event
- scroll(container, { x: 501, y: 1001 });
- // no frame to release change yet
+ it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
- // third event - back to original value
- scroll(container, { x: 500, y: 1000 });
- // release the frame
- requestAnimationFrame.step();
- expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(scheduled);
+
+ // first event
+ scroll(container, { x: 500, y: 1000 });
+ requestAnimationFrame.step();
+ expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
+ marshal.updateDroppableScroll.mockReset();
+
+ // second event
+ scroll(container, { x: 400, y: 100 });
+ // no animation frame to release event fired yet
+
+ // unwatching before frame fired
+ callbacks.unwatchScroll();
+
+ // flushing any frames
+ requestAnimationFrame.flush();
+ expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
+ });
});
it('should stop watching scroll when no longer required to publish', () => {
@@ -710,12 +866,10 @@ describe('DraggableDimensionPublisher', () => {
// watch scroll will only be called after the dimension is requested
callbacks.getDimension();
- callbacks.watchScroll();
+ callbacks.watchScroll(immediate);
// first event
scroll(container, { x: 500, y: 1000 });
- // release the frame
- requestAnimationFrame.step();
expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
marshal.updateDroppableScroll.mockReset();
@@ -723,12 +877,11 @@ describe('DraggableDimensionPublisher', () => {
// scroll event after no longer watching
scroll(container, { x: 190, y: 400 });
- // let any frames go that want to
- requestAnimationFrame.flush();
expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
});
- it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => {
+ it('should stop watching for scroll events when the component is unmounted', () => {
+ jest.spyOn(console, 'warn').mockImplementation(() => { });
const marshal: DimensionMarshal = getMarshalStub();
const wrapper = mount(
,
@@ -740,52 +893,113 @@ describe('DraggableDimensionPublisher', () => {
// watch scroll will only be called after the dimension is requested
callbacks.getDimension();
- callbacks.watchScroll();
-
- // first event
- scroll(container, { x: 500, y: 1000 });
- requestAnimationFrame.step();
- expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1);
- marshal.updateDroppableScroll.mockReset();
-
- // second event
- scroll(container, { x: 400, y: 100 });
- // no animation frame to release event fired yet
+ callbacks.watchScroll(immediate);
- // unwatching before frame fired
- callbacks.unwatchScroll();
+ wrapper.unmount();
- // flushing any frames
- requestAnimationFrame.flush();
+ // second event - will not fire any updates
+ scroll(container, { x: 100, y: 300 });
expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
+ // also logs a warning
+ expect(console.warn).toHaveBeenCalled();
+
+ // cleanup
+ console.warn.mockRestore();
});
+ });
- it('should stop watching for scroll events when the component is unmounted', () => {
- jest.spyOn(console, 'warn').mockImplementation(() => { });
+ describe('forced scroll', () => {
+ it('should not do anything if the droppable has no closest scrollable', () => {
const marshal: DimensionMarshal = getMarshalStub();
+ // no scroll parent
const wrapper = mount(
- ,
+ ,
withDimensionMarshal(marshal),
);
- const container: HTMLElement = wrapper.getDOMNode();
- // tell the droppable to watch for scrolling
- const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ const parentNode = wrapper.getDOMNode();
+ const droppableNode = wrapper.state().ref;
+ jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame);
+ jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client);
- // watch scroll will only be called after the dimension is requested
+ // validating no initial scroll
+ expect(parentNode.scrollTop).toBe(0);
+ expect(parentNode.scrollLeft).toBe(0);
+ expect(droppableNode.scrollTop).toBe(0);
+ expect(droppableNode.scrollLeft).toBe(0);
+
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // request the droppable start listening for scrolling
callbacks.getDimension();
- callbacks.watchScroll();
+ callbacks.watchScroll(scheduled);
+ expect(console.error).not.toHaveBeenCalled();
- wrapper.unmount();
+ // ask it to scroll
+ callbacks.scroll({ x: 100, y: 100 });
- // second event - will not fire any updates
- scroll(container, { x: 100, y: 300 });
- requestAnimationFrame.step();
- expect(marshal.updateDroppableScroll).not.toHaveBeenCalled();
- // also logs a warning
- expect(console.warn).toHaveBeenCalled();
+ expect(parentNode.scrollTop).toBe(0);
+ expect(parentNode.scrollLeft).toBe(0);
+ expect(droppableNode.scrollTop).toBe(0);
+ expect(droppableNode.scrollLeft).toBe(0);
+ expect(console.error).toHaveBeenCalled();
+ });
- // cleanup
- console.warn.mockRestore();
+ describe('there is a closest scrollable', () => {
+ it('should update the scroll of the closest scrollable', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
+
+ if (!container.classList.contains('scroll-container')) {
+ throw new Error('incorrect dom node collected');
+ }
+
+ expect(container.scrollTop).toBe(0);
+ expect(container.scrollLeft).toBe(0);
+
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ // watch scroll will only be called after the dimension is requested
+ callbacks.getDimension();
+ callbacks.watchScroll(scheduled);
+
+ callbacks.scroll({ x: 500, y: 1000 });
+
+ expect(container.scrollLeft).toBe(500);
+ expect(container.scrollTop).toBe(1000);
+ });
+
+ it('should not scroll if scroll is not currently being watched', () => {
+ const marshal: DimensionMarshal = getMarshalStub();
+ const wrapper = mount(
+ ,
+ withDimensionMarshal(marshal),
+ );
+ const container: HTMLElement = wrapper.getDOMNode();
+
+ if (!container.classList.contains('scroll-container')) {
+ throw new Error('incorrect dom node collected');
+ }
+
+ expect(container.scrollTop).toBe(0);
+ expect(container.scrollLeft).toBe(0);
+
+ // tell the droppable to watch for scrolling
+ const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1];
+ callbacks.getDimension();
+ // not watching scroll yet
+
+ callbacks.scroll({ x: 500, y: 1000 });
+
+ expect(container.scrollLeft).toBe(0);
+ expect(container.scrollTop).toBe(0);
+ expect(console.error).toHaveBeenCalled();
+ });
});
});
diff --git a/test/unit/view/style-marshal.spec.js b/test/unit/view/style-marshal.spec.js
index 5399b95f77..5bb36af768 100644
--- a/test/unit/view/style-marshal.spec.js
+++ b/test/unit/view/style-marshal.spec.js
@@ -1,10 +1,12 @@
// @flow
import createStyleMarshal from '../../../src/view/style-marshal/style-marshal';
import getStyles, { type Styles } from '../../../src/view/style-marshal/get-styles';
+import getStatePreset from '../../utils/get-simple-state-preset';
import type { StyleMarshal } from '../../../src/view/style-marshal/style-marshal-types';
-import * as state from '../../utils/simple-state-preset';
import type { State } from '../../../src/types';
+const state = getStatePreset();
+
const getStyleTagSelector = (context: string) =>
`style[data-react-beautiful-dnd="${context}"]`;
diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js
index f1ebf028ee..451b8bcd81 100644
--- a/test/unit/view/unconnected-draggable.spec.js
+++ b/test/unit/view/unconnected-draggable.spec.js
@@ -37,7 +37,7 @@ import getArea from '../../../src/state/get-area';
import { combine, withStore, withDroppableId, withStyleContext, withDimensionMarshal, withCanLift } from '../../utils/get-context-options';
import { dispatchWindowMouseEvent, mouseEvent } from '../../utils/user-input-util';
import setWindowScroll from '../../utils/set-window-scroll';
-import getWindowScrollPosition from '../../../src/view/get-window-scroll-position';
+import getWindowScroll from '../../../src/window/get-window-scroll';
class Item extends Component<{ provided: Provided }> {
render() {
@@ -123,6 +123,7 @@ const defaultMapProps: MapProps = {
offset: origin,
dimension: null,
direction: null,
+ draggingOver: null,
};
const somethingElseDraggingMapProps: MapProps = defaultMapProps;
@@ -136,7 +137,7 @@ const draggingMapProps: MapProps = {
// this may or may not be set during a drag
dimension,
direction: null,
-
+ draggingOver: null,
};
const dropAnimatingMapProps: MapProps = {
@@ -147,6 +148,7 @@ const dropAnimatingMapProps: MapProps = {
shouldAnimateDragMovement: false,
dimension,
direction: null,
+ draggingOver: null,
};
const dropCompleteMapProps: MapProps = defaultMapProps;
@@ -169,16 +171,18 @@ const mountDraggable = ({
// registering the droppable so that publishing the dimension will work correctly
const dimensionMarshal: DimensionMarshal = createDimensionMarshal({
cancel: () => { },
- publishDraggables: () => { },
- publishDroppables: () => { },
+ publishDraggable: () => { },
+ publishDroppable: () => { },
updateDroppableScroll: () => { },
updateDroppableIsEnabled: () => { },
+ bulkPublish: () => { },
});
dimensionMarshal.registerDroppable(droppable.descriptor, {
getDimension: () => droppable,
watchScroll: () => { },
unwatchScroll: () => { },
+ scroll: () => {},
});
const wrapper: ReactWrapper = mount(
@@ -206,7 +210,7 @@ const mountDraggable = ({
const mouseDown = mouseEvent.bind(null, 'mousedown');
const windowMouseMove = dispatchWindowMouseEvent.bind(null, 'mousemove');
-const originalWindowScroll: Position = getWindowScrollPosition();
+const originalWindowScroll: Position = getWindowScroll();
type StartDrag = {|
selection?: Position,
@@ -228,12 +232,14 @@ const executeOnLift = (wrapper: ReactWrapper) => ({
selection = origin,
center = origin,
windowScroll = origin,
- isScrollAllowed = false,
}: StartDrag = {}) => {
setWindowScroll(windowScroll);
stubArea(center);
- wrapper.find(DragHandle).props().callbacks.onLift({ client: selection, isScrollAllowed });
+ wrapper.find(DragHandle).props().callbacks.onLift({
+ client: selection,
+ autoScrollMode: 'FLUID',
+ });
};
// $ExpectError - not checking type of mock
@@ -419,13 +425,12 @@ describe('Draggable - unconnected', () => {
center,
};
const windowScroll = { x: 100, y: 30 };
- const isScrollAllowed: boolean = true;
- executeOnLift(wrapper)({ selection, center, windowScroll, isScrollAllowed });
+ executeOnLift(wrapper)({ selection, center, windowScroll });
// $ExpectError - mock property on lift function
expect(dispatchProps.lift.mock.calls[0]).toEqual([
- draggableId, initial, windowScroll, isScrollAllowed,
+ draggableId, initial, windowScroll, 'FLUID',
]);
});
});
@@ -1016,6 +1021,42 @@ describe('Draggable - unconnected', () => {
const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot;
expect(snapshot.isDragging).toBe(true);
});
+
+ it('should let consumers know if draggging and over a droppable', () => {
+ // $ExpectError - using spread
+ const mapProps: MapProps = {
+ ...draggingMapProps,
+ draggingOver: 'foobar',
+ };
+
+ const myMock = jest.fn();
+
+ mountDraggable({
+ mapProps,
+ WrappedComponent: getStubber(myMock),
+ });
+
+ const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot;
+ expect(snapshot.draggingOver).toBe('foobar');
+ });
+
+ it('should let consumers know if dragging and not over a droppable', () => {
+ // $ExpectError - using spread
+ const mapProps: MapProps = {
+ ...draggingMapProps,
+ draggingOver: null,
+ };
+
+ const myMock = jest.fn();
+
+ mountDraggable({
+ mapProps,
+ WrappedComponent: getStubber(myMock),
+ });
+
+ const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot;
+ expect(snapshot.draggingOver).toBe(null);
+ });
});
describe('drop animating', () => {
diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js
index 49551b1978..e09cbd5717 100644
--- a/test/unit/view/unconnected-droppable.spec.js
+++ b/test/unit/view/unconnected-droppable.spec.js
@@ -5,10 +5,19 @@ import { mount } from 'enzyme';
import type { ReactWrapper } from 'enzyme';
import Droppable from '../../../src/view/droppable/droppable';
import Placeholder from '../../../src/view/placeholder/';
-import { withStore, combine, withDimensionMarshal } from '../../utils/get-context-options';
+import { withStore, combine, withDimensionMarshal, withStyleContext } from '../../utils/get-context-options';
import { getPreset } from '../../utils/dimension';
-import type { DroppableId, DraggableDimension } from '../../../src/types';
-import type { MapProps, OwnProps, Provided, StateSnapshot } from '../../../src/view/droppable/droppable-types';
+import type {
+ DraggableId,
+ DroppableId,
+ DraggableDimension,
+} from '../../../src/types';
+import type {
+ MapProps,
+ OwnProps,
+ Provided,
+ StateSnapshot,
+} from '../../../src/view/droppable/droppable-types';
const getStubber = (mock: Function) =>
class Stubber extends Component<{provided: Provided, snapshot: StateSnapshot}> {
@@ -23,12 +32,15 @@ const getStubber = (mock: Function) =>
}
};
const defaultDroppableId: DroppableId = 'droppable-1';
+const draggableId: DraggableId = 'draggable-1';
const notDraggingOverMapProps: MapProps = {
isDraggingOver: false,
+ draggingOverWith: null,
placeholder: null,
};
const isDraggingOverHomeMapProps: MapProps = {
isDraggingOver: true,
+ draggingOverWith: draggableId,
placeholder: null,
};
@@ -37,6 +49,7 @@ const inHome1: DraggableDimension = data.inHome1;
const isDraggingOverForeignMapProps: MapProps = {
isDraggingOver: true,
+ draggingOverWith: 'draggable-1',
placeholder: inHome1.placeholder,
};
@@ -66,7 +79,11 @@ const mountDroppable = ({
)}
,
- combine(withStore(), withDimensionMarshal())
+ combine(
+ withStore(),
+ withDimensionMarshal(),
+ withStyleContext(),
+ )
);
describe('Droppable - unconnected', () => {
@@ -82,6 +99,7 @@ describe('Droppable - unconnected', () => {
const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
expect(provided.innerRef).toBeInstanceOf(Function);
expect(snapshot.isDraggingOver).toBe(true);
+ expect(snapshot.draggingOverWith).toBe(draggableId);
expect(provided.placeholder).toBe(null);
});
});
@@ -98,6 +116,7 @@ describe('Droppable - unconnected', () => {
const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
expect(provided.innerRef).toBeInstanceOf(Function);
expect(snapshot.isDraggingOver).toBe(true);
+ expect(snapshot.draggingOverWith).toBe(draggableId);
// $ExpectError - type property of placeholder
expect(provided.placeholder.type).toBe(Placeholder);
// $ExpectError - props property of placeholder
@@ -117,6 +136,7 @@ describe('Droppable - unconnected', () => {
const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot;
expect(provided.innerRef).toBeInstanceOf(Function);
expect(snapshot.isDraggingOver).toBe(false);
+ expect(snapshot.draggingOverWith).toBe(null);
expect(provided.placeholder).toBe(null);
});
});
diff --git a/test/utils/dimension.js b/test/utils/dimension.js
index d607edc8db..1d1689e99a 100644
--- a/test/utils/dimension.js
+++ b/test/utils/dimension.js
@@ -1,10 +1,15 @@
// @flow
import getArea from '../../src/state/get-area';
+import { noMovement } from '../../src/state/no-impact';
import { getDroppableDimension, getDraggableDimension } from '../../src/state/dimension';
import { vertical } from '../../src/state/axis';
import type {
+ Area,
Axis,
+ DragImpact,
+ State,
Position,
+ ClosestScrollable,
Spacing,
DroppableDimension,
DraggableDimension,
@@ -12,17 +17,127 @@ import type {
DroppableDimensionMap,
} from '../../src/types';
-export const getPreset = (axis?: Axis = vertical) => {
- const margin: Spacing = { top: 10, left: 10, bottom: 5, right: 5 };
- const padding: Spacing = { top: 2, left: 2, bottom: 2, right: 2 };
- const windowScroll: Position = { x: 50, y: 100 };
- const crossAxisStart: number = 0;
- const crossAxisEnd: number = 100;
- const foreignCrossAxisStart: number = 100;
- const foreignCrossAxisEnd: number = 200;
- const emptyForeignCrossAxisStart: number = 200;
- const emptyForeignCrossAxisEnd: number = 300;
+const margin: Spacing = { top: 10, left: 10, bottom: 5, right: 5 };
+const padding: Spacing = { top: 2, left: 2, bottom: 2, right: 2 };
+const windowScroll: Position = { x: 50, y: 100 };
+const crossAxisStart: number = 0;
+const crossAxisEnd: number = 100;
+const foreignCrossAxisStart: number = 100;
+const foreignCrossAxisEnd: number = 200;
+const emptyForeignCrossAxisStart: number = 200;
+const emptyForeignCrossAxisEnd: number = 300;
+
+export const makeScrollable = (droppable: DroppableDimension, amount?: number = 20) => {
+ const axis: Axis = droppable.axis;
+ const client: Area = droppable.client.withoutMargin;
+
+ const horizontalGrowth: number = axis === vertical ? 0 : amount;
+ const verticalGrowth: number = axis === vertical ? amount : 0;
+
+ // is 10px smaller than the client on the main axis
+ // this will leave 10px of scrollable area.
+ // only expanding on one axis
+ const newClient: Area = getArea({
+ top: client.top,
+ left: client.left,
+ // growing the client to account for the scrollable area
+ right: client.right + horizontalGrowth,
+ bottom: client.bottom + verticalGrowth,
+ });
+
+ // add scroll space on the main axis
+ const scrollSize = {
+ width: client.width + horizontalGrowth,
+ height: client.height + verticalGrowth,
+ };
+
+ return getDroppableDimension({
+ descriptor: droppable.descriptor,
+ direction: axis.direction,
+ padding,
+ margin,
+ windowScroll,
+ client: newClient,
+ closest: {
+ frameClient: client,
+ scrollWidth: scrollSize.width,
+ scrollHeight: scrollSize.height,
+ scroll: { x: 0, y: 0 },
+ shouldClipSubject: true,
+ },
+ });
+};
+
+export const getInitialImpact = (draggable: DraggableDimension, axis?: Axis = vertical) => {
+ const impact: DragImpact = {
+ movement: noMovement,
+ direction: axis.direction,
+ destination: {
+ index: draggable.descriptor.index,
+ droppableId: draggable.descriptor.droppableId,
+ },
+ };
+ return impact;
+};
+
+export const withImpact = (state: State, impact: DragImpact) => {
+ // while dragging
+ if (state.drag) {
+ return {
+ ...state,
+ drag: {
+ ...state.drag,
+ impact,
+ },
+ };
+ }
+ // while drop animating
+ if (state.drop && state.drop.pending) {
+ return {
+ ...state,
+ drop: {
+ ...state.drop,
+ pending: {
+ ...state.drop.pending,
+ impact,
+ },
+ },
+ };
+ }
+
+ throw new Error('unable to apply impact');
+};
+
+export const addDroppable = (base: State, droppable: DroppableDimension): State => ({
+ ...base,
+ dimension: {
+ ...base.dimension,
+ droppable: {
+ ...base.dimension.droppable,
+ [droppable.descriptor.id]: droppable,
+ },
+ },
+});
+
+export const addDraggable = (base: State, draggable: DraggableDimension): State => ({
+ ...base,
+ dimension: {
+ ...base.dimension,
+ draggable: {
+ ...base.dimension.draggable,
+ [draggable.descriptor.id]: draggable,
+ },
+ },
+});
+
+export const getClosestScrollable = (droppable: DroppableDimension): ClosestScrollable => {
+ if (!droppable.viewport.closestScrollable) {
+ throw new Error('Cannot get closest scrollable');
+ }
+ return droppable.viewport.closestScrollable;
+};
+export const getPreset = (axis?: Axis = vertical) => {
const home: DroppableDimension = getDroppableDimension({
descriptor: {
id: 'home',
diff --git a/test/utils/get-context-options.js b/test/utils/get-context-options.js
index c8bc59bb30..8624f2f107 100644
--- a/test/utils/get-context-options.js
+++ b/test/utils/get-context-options.js
@@ -54,10 +54,11 @@ export const withDimensionMarshal = (marshal?: DimensionMarshal): Object => ({
context: {
[dimensionMarshalKey]: marshal || createDimensionMarshal({
cancel: () => { },
- publishDraggables: () => { },
- publishDroppables: () => { },
+ publishDraggable: () => { },
+ publishDroppable: () => { },
updateDroppableScroll: () => { },
updateDroppableIsEnabled: () => { },
+ bulkPublish: () => { },
}),
},
childContextTypes: {
diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js
new file mode 100644
index 0000000000..938e11136f
--- /dev/null
+++ b/test/utils/get-simple-state-preset.js
@@ -0,0 +1,291 @@
+// @flow
+import { getPreset } from './dimension';
+import noImpact from '../../src/state/no-impact';
+import { vertical } from '../../src/state/axis';
+import type {
+ Axis,
+ State,
+ DraggableDescriptor,
+ DroppableDescriptor,
+ DimensionState,
+ DraggableDimension,
+ DroppableDimension,
+ CurrentDragPositions,
+ InitialDragPositions,
+ LiftRequest,
+ Position,
+ DragState,
+ DropResult,
+ PendingDrop,
+ DropReason,
+ DraggableId,
+ DragImpact,
+ ScrollOptions,
+} from '../../src/types';
+
+const scheduled: ScrollOptions = {
+ shouldPublishImmediately: false,
+};
+
+export default (axis?: Axis = vertical) => {
+ const preset = getPreset(axis);
+
+ const getDimensionState = (request: LiftRequest): DimensionState => {
+ const draggable: DraggableDimension = preset.draggables[request.draggableId];
+ const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId];
+
+ const result: DimensionState = {
+ request,
+ draggable: { [draggable.descriptor.id]: draggable },
+ droppable: { [home.descriptor.id]: home },
+ };
+ return result;
+ };
+
+ const idle: State = {
+ phase: 'IDLE',
+ drag: null,
+ drop: null,
+ dimension: {
+ request: null,
+ draggable: {},
+ droppable: {},
+ },
+ };
+
+ const preparing: State = {
+ ...idle,
+ phase: 'PREPARING',
+ };
+
+ const defaultLiftRequest: LiftRequest = {
+ draggableId: preset.inHome1.descriptor.id,
+ scrollOptions: {
+ shouldPublishImmediately: false,
+ },
+ };
+ const requesting = (request?: LiftRequest = defaultLiftRequest): State => {
+ const result: State = {
+ phase: 'COLLECTING_INITIAL_DIMENSIONS',
+ drag: null,
+ drop: null,
+ dimension: {
+ request,
+ draggable: {},
+ droppable: {},
+ },
+ };
+ return result;
+ };
+
+ const origin: Position = { x: 0, y: 0 };
+
+ const dragging = (
+ id?: DraggableId = preset.inHome1.descriptor.id,
+ selection?: Position,
+ ): State => {
+ // will populate the dimension state with the initial dimensions
+ const draggable: DraggableDimension = preset.draggables[id];
+ // either use the provided selection or use the draggable's center
+ const clientSelection: Position = selection || draggable.client.withMargin.center;
+ const initialPosition: InitialDragPositions = {
+ selection: clientSelection,
+ center: clientSelection,
+ };
+ const clientPositions: CurrentDragPositions = {
+ selection: clientSelection,
+ center: clientSelection,
+ offset: origin,
+ };
+
+ const drag: DragState = {
+ initial: {
+ descriptor: draggable.descriptor,
+ autoScrollMode: 'FLUID',
+ client: initialPosition,
+ page: initialPosition,
+ windowScroll: origin,
+ },
+ current: {
+ client: clientPositions,
+ page: clientPositions,
+ windowScroll: origin,
+ shouldAnimate: false,
+ hasCompletedFirstBulkPublish: true,
+ },
+ impact: noImpact,
+ scrollJumpRequest: null,
+ };
+
+ const result: State = {
+ phase: 'DRAGGING',
+ drag,
+ drop: null,
+ dimension: getDimensionState({
+ draggableId: id,
+ scrollOptions: scheduled,
+ }),
+ };
+
+ return result;
+ };
+
+ const scrollJumpRequest = (request: Position): State => {
+ const id: DraggableId = preset.inHome1.descriptor.id;
+ // will populate the dimension state with the initial dimensions
+ const draggable: DraggableDimension = preset.draggables[id];
+ // either use the provided selection or use the draggable's center
+ const initialPosition: InitialDragPositions = {
+ selection: draggable.client.withMargin.center,
+ center: draggable.client.withMargin.center,
+ };
+ const clientPositions: CurrentDragPositions = {
+ selection: draggable.client.withMargin.center,
+ center: draggable.client.withMargin.center,
+ offset: origin,
+ };
+
+ const impact: DragImpact = {
+ movement: {
+ displaced: [],
+ amount: origin,
+ isBeyondStartPosition: false,
+ },
+ direction: preset.home.axis.direction,
+ destination: {
+ index: preset.inHome1.descriptor.index,
+ droppableId: preset.inHome1.descriptor.droppableId,
+ },
+ };
+
+ const drag: DragState = {
+ initial: {
+ descriptor: draggable.descriptor,
+ autoScrollMode: 'JUMP',
+ client: initialPosition,
+ page: initialPosition,
+ windowScroll: origin,
+ },
+ current: {
+ client: clientPositions,
+ page: clientPositions,
+ windowScroll: origin,
+ shouldAnimate: true,
+ hasCompletedFirstBulkPublish: true,
+ },
+ impact,
+ scrollJumpRequest: request,
+ };
+
+ const result: State = {
+ phase: 'DRAGGING',
+ drag,
+ drop: null,
+ dimension: getDimensionState({
+ draggableId: id,
+ scrollOptions: scheduled,
+ }),
+ };
+
+ return result;
+ };
+
+ const getDropAnimating = (id: DraggableId, reason: DropReason): State => {
+ const descriptor: DraggableDescriptor = preset.draggables[id].descriptor;
+ const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor;
+ const pending: PendingDrop = {
+ newHomeOffset: origin,
+ impact: noImpact,
+ result: {
+ draggableId: descriptor.id,
+ type: home.type,
+ source: {
+ droppableId: home.id,
+ index: descriptor.index,
+ },
+ destination: null,
+ reason,
+ },
+ };
+
+ const result: State = {
+ phase: 'DROP_ANIMATING',
+ drag: null,
+ drop: {
+ pending,
+ result: null,
+ },
+ dimension: getDimensionState({
+ draggableId: descriptor.id,
+ scrollOptions: scheduled,
+ }),
+ };
+ return result;
+ };
+
+ const dropAnimating = (
+ id?: DraggableId = preset.inHome1.descriptor.id
+ ): State => getDropAnimating(id, 'DROP');
+
+ const userCancel = (
+ id?: DraggableId = preset.inHome1.descriptor.id
+ ): State => getDropAnimating(id, 'CANCEL');
+
+ const dropComplete = (
+ id?: DraggableId = preset.inHome1.descriptor.id
+ ): State => {
+ const descriptor: DraggableDescriptor = preset.draggables[id].descriptor;
+ const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor;
+ const result: DropResult = {
+ draggableId: descriptor.id,
+ type: home.type,
+ source: {
+ droppableId: home.id,
+ index: descriptor.index,
+ },
+ destination: null,
+ reason: 'DROP',
+ };
+
+ const value: State = {
+ phase: 'DROP_COMPLETE',
+ drag: null,
+ drop: {
+ pending: null,
+ result,
+ },
+ dimension: {
+ request: null,
+ draggable: {},
+ droppable: {},
+ },
+ };
+ return value;
+ };
+
+ const allPhases = (id? : DraggableId = preset.inHome1.descriptor.id): State[] => [
+ idle,
+ preparing,
+ requesting({
+ draggableId: id,
+ scrollOptions: scheduled,
+ }),
+ dragging(id),
+ dropAnimating(id),
+ userCancel(id),
+ dropComplete(id),
+ ];
+
+ return {
+ idle,
+ preparing,
+ requesting,
+ dragging,
+ scrollJumpRequest,
+ dropAnimating,
+ userCancel,
+ dropComplete,
+ allPhases,
+ };
+};
+
diff --git a/test/utils/set-viewport.js b/test/utils/set-viewport.js
new file mode 100644
index 0000000000..880dfc9183
--- /dev/null
+++ b/test/utils/set-viewport.js
@@ -0,0 +1,47 @@
+// @flow
+import type { Area } from '../../src/types';
+import getArea from '../../src/state/get-area';
+
+const getDoc = (): HTMLElement => {
+ const el: ?HTMLElement = document.documentElement;
+
+ if (!el) {
+ throw new Error('Unable to get document.documentElement');
+ }
+
+ return el;
+};
+
+const setViewport = (custom: Area) => {
+ if (custom.top !== 0 || custom.left !== 0) {
+ throw new Error('not setting window scroll with setViewport. Use set-window-scroll');
+ }
+
+ if (window.pageXOffset !== 0 || window.pageYOffset !== 0) {
+ throw new Error('Setting viewport on scrolled window');
+ }
+
+ window.pageYOffset = 0;
+ window.pageXOffset = 0;
+
+ const doc: HTMLElement = getDoc();
+ doc.clientWidth = custom.width;
+ doc.clientHeight = custom.height;
+};
+
+export const getCurrent = (): Area => {
+ const doc: HTMLElement = getDoc();
+
+ return getArea({
+ top: window.pageYOffset,
+ left: window.pageXOffset,
+ width: doc.clientWidth,
+ height: doc.clientHeight,
+ });
+};
+
+const original: Area = getCurrent();
+
+export const resetViewport = () => setViewport(original);
+
+export default setViewport;
diff --git a/test/utils/set-window-scroll-size.js b/test/utils/set-window-scroll-size.js
new file mode 100644
index 0000000000..624feca4ae
--- /dev/null
+++ b/test/utils/set-window-scroll-size.js
@@ -0,0 +1,34 @@
+// @flow
+
+type Args = {|
+ scrollHeight: number,
+ scrollWidth: number,
+|}
+
+const setWindowScrollSize = ({ scrollHeight, scrollWidth }: Args): void => {
+ const el: ?HTMLElement = document.documentElement;
+
+ if (!el) {
+ throw new Error('Unable to find document element');
+ }
+
+ el.scrollHeight = scrollHeight;
+ el.scrollWidth = scrollWidth;
+};
+
+const original: Args = (() => {
+ const el: ?HTMLElement = document.documentElement;
+
+ if (!el) {
+ throw new Error('Unable to find document element');
+ }
+
+ return {
+ scrollWidth: el.scrollWidth,
+ scrollHeight: el.scrollHeight,
+ };
+})();
+
+export const resetWindowScrollSize = () => setWindowScrollSize(original);
+
+export default setWindowScrollSize;
diff --git a/test/utils/set-window-scroll.js b/test/utils/set-window-scroll.js
index f8e779ce3d..332af8050c 100644
--- a/test/utils/set-window-scroll.js
+++ b/test/utils/set-window-scroll.js
@@ -9,10 +9,20 @@ const defaultOptions: Options = {
shouldPublish: true,
};
-export default (point: Position, options?: Options = defaultOptions) => {
+const setWindowScroll = (point: Position, options?: Options = defaultOptions) => {
window.pageXOffset = point.x;
window.pageYOffset = point.y;
+
if (options.shouldPublish) {
window.dispatchEvent(new Event('scroll'));
}
};
+
+const original: Position = {
+ x: window.pageXOffset,
+ y: window.pageYOffset,
+};
+
+export const resetWindowScroll = () => setWindowScroll(original);
+
+export default setWindowScroll;
diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js
deleted file mode 100644
index 49f338ce70..0000000000
--- a/test/utils/simple-state-preset.js
+++ /dev/null
@@ -1,188 +0,0 @@
-// @flow
-import { getPreset } from './dimension';
-import noImpact from '../../src/state/no-impact';
-import type {
- State,
- DraggableDescriptor,
- DroppableDescriptor,
- DimensionState,
- DraggableDimension,
- DroppableDimension,
- CurrentDragPositions,
- InitialDragPositions,
- Position,
- DragState,
- DropResult,
- PendingDrop,
- DropTrigger,
- DraggableId,
-} from '../../src/types';
-
-const preset = getPreset();
-
-const getDimensionState = (request: DraggableId): DimensionState => {
- const draggable: DraggableDimension = preset.draggables[request];
- const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId];
-
- const result: DimensionState = {
- request,
- draggable: { [draggable.descriptor.id]: draggable },
- droppable: { [home.descriptor.id]: home },
- };
- return result;
-};
-
-export const idle: State = {
- phase: 'IDLE',
- drag: null,
- drop: null,
- dimension: {
- request: null,
- draggable: {},
- droppable: {},
- },
-};
-
-export const preparing: State = {
- ...idle,
- phase: 'PREPARING',
-};
-
-export const requesting = (request?: DraggableId = preset.inHome1.descriptor.id): State => {
- const result: State = {
- phase: 'COLLECTING_INITIAL_DIMENSIONS',
- drag: null,
- drop: null,
- dimension: {
- request,
- draggable: {},
- droppable: {},
- },
- };
- return result;
-};
-
-const origin: Position = { x: 0, y: 0 };
-
-export const dragging = (
- id?: DraggableId = preset.inHome1.descriptor.id
-): State => {
- // will populate the dimension state with the initial dimensions
- const draggable: DraggableDimension = preset.draggables[id];
- const client: Position = draggable.client.withMargin.center;
- const initialPosition: InitialDragPositions = {
- selection: client,
- center: client,
- };
- const clientPositions: CurrentDragPositions = {
- selection: client,
- center: client,
- offset: origin,
- };
-
- const drag: DragState = {
- initial: {
- descriptor: draggable.descriptor,
- isScrollAllowed: true,
- client: initialPosition,
- page: initialPosition,
- windowScroll: origin,
- },
- current: {
- client: clientPositions,
- page: clientPositions,
- windowScroll: origin,
- shouldAnimate: false,
- },
- impact: noImpact,
- };
-
- const result: State = {
- phase: 'DRAGGING',
- drag,
- drop: null,
- dimension: getDimensionState(id),
- };
-
- return result;
-};
-
-const getDropAnimating = (id: DraggableId, trigger: DropTrigger): State => {
- const descriptor: DraggableDescriptor = preset.draggables[id].descriptor;
- const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor;
- const pending: PendingDrop = {
- trigger,
- newHomeOffset: origin,
- impact: noImpact,
- result: {
- draggableId: descriptor.id,
- type: home.type,
- source: {
- droppableId: home.id,
- index: descriptor.index,
- },
- destination: null,
- },
- };
-
- const result: State = {
- phase: 'DROP_ANIMATING',
- drag: null,
- drop: {
- pending,
- result: null,
- },
- dimension: getDimensionState(descriptor.id),
- };
- return result;
-};
-
-export const dropAnimating = (
- id?: DraggableId = preset.inHome1.descriptor.id
-): State => getDropAnimating(id, 'DROP');
-
-export const userCancel = (
- id?: DraggableId = preset.inHome1.descriptor.id
-): State => getDropAnimating(id, 'CANCEL');
-
-export const dropComplete = (
- id?: DraggableId = preset.inHome1.descriptor.id
-): State => {
- const descriptor: DraggableDescriptor = preset.draggables[id].descriptor;
- const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor;
- const result: DropResult = {
- draggableId: descriptor.id,
- type: home.type,
- source: {
- droppableId: home.id,
- index: descriptor.index,
- },
- destination: null,
- };
-
- const value: State = {
- phase: 'DROP_COMPLETE',
- drag: null,
- drop: {
- pending: null,
- result,
- },
- dimension: {
- request: null,
- draggable: {},
- droppable: {},
- },
- };
- return value;
-};
-
-export const allPhases = (id? : DraggableId = preset.inHome1.descriptor.id): State[] => [
- idle,
- preparing,
- requesting(id),
- dragging(id),
- dropAnimating(id),
- userCancel(id),
- dropComplete(id),
-];
-