Skip to content

Commit

Permalink
fix(dashboard): constrain drag start and endpoint in grid
Browse files Browse the repository at this point in the history
  • Loading branch information
square-li authored and diehbria committed Mar 24, 2023
1 parent 6970edd commit ea2b875
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 53 deletions.
43 changes: 38 additions & 5 deletions packages/dashboard/src/components/grid/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { PointerEventHandler, ReactNode } from 'react';
import React, { useEffect, useState } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { useKeyPress } from '~/hooks/useKeyPress';
import type { Position } from '~/types';
import { MouseClick } from '~/types';
import { ItemTypes } from '../dragLayer/itemTypes';
import { gestureable } from '../internalDashboard/gestures/determineTargetGestures';
import { DASHBOARD_CONTAINER_ID, getDashboardPosition } from './getDashboardPosition';

import './index.css';
import type { PointerEventHandler, ReactNode } from 'react';
import type { DashboardState } from '~/store/state';
import type { Position } from '~/types';
import type { ComponentPaletteDraggable } from '../palette/types';

export type DragEvent = {
Expand Down Expand Up @@ -60,9 +59,21 @@ const deltaTracker = trackPosition(defaultDelta);
const startTracker = trackPosition(defaultDelta);
const endTracker = trackPosition(defaultDelta);

const constrainPosition = (params: {
position: Position;
gridSize: { width: number; height: number; x: number; y: number };
}) => {
const { position, gridSize } = params;
return {
x: Math.min(Math.max(position.x, 0), gridSize.width),
y: Math.min(Math.max(position.y, 0), gridSize.height),
};
};

const Grid: React.FC<GridProps> = ({ readOnly, grid, click, dragStart, drag, dragEnd, drop, children }) => {
const { width, height, cellSize, stretchToFit, enabled } = grid;

const [dashboardGrid, setDashboardGrid] = useState<DOMRect | null>(null);
const [cancelClick, setCancelClick] = useState(false);
const [target, setTarget] = useState<EventTarget | undefined>();
const union = useKeyPress('shift');
Expand Down Expand Up @@ -146,14 +157,32 @@ const Grid: React.FC<GridProps> = ({ readOnly, grid, click, dragStart, drag, dra
const { isDragging, clientOffset } = collected;

if (isDragging && clientOffset) {
if (!dashboardGrid) return;
const constrainedOffset = constrainPosition({
position: {
x: clientOffset.x - dashboardGrid.x,
y: clientOffset.y - dashboardGrid.y,
},
gridSize: dashboardGrid,
});

const constrainedDeltaTracker = constrainPosition({
position: {
x: deltaTracker.getPosition().x - dashboardGrid.x,
y: deltaTracker.getPosition().y - dashboardGrid.y,
},
gridSize: dashboardGrid,
});
const offset = {
x: clientOffset.x - deltaTracker.getPosition().x,
y: clientOffset.y - deltaTracker.getPosition().y,
x: constrainedOffset.x - constrainedDeltaTracker.x,
y: constrainedOffset.y - constrainedDeltaTracker.y,
};

const updatedEndPosition = {
x: endTracker.getPosition().x + offset.x,
y: endTracker.getPosition().y + offset.y,
};

drag({
target,
start: startTracker.getPosition(),
Expand All @@ -171,6 +200,10 @@ const Grid: React.FC<GridProps> = ({ readOnly, grid, click, dragStart, drag, dra
if (readOnly) return;
setTarget(e.target);
setCancelClick(false);
const dashboardGrid = document.getElementById(DASHBOARD_CONTAINER_ID)?.getBoundingClientRect();
if (dashboardGrid) {
setDashboardGrid(dashboardGrid);
}
startTracker.setPosition(getDashboardPosition(e));
endTracker.setPosition(getDashboardPosition(e));
};
Expand Down
33 changes: 32 additions & 1 deletion packages/dashboard/src/store/actions/moveWidgets/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { moveWidgets, onMoveWidgetsAction } from './index';
import type { DashboardState } from '../../state';
import { initialState } from '../../state';

import { MOCK_KPI_WIDGET, MockWidgetFactory } from '../../../../testing/mocks';
import type { DashboardState } from '../../state';
import type { Widget } from '~/types';

const setupDashboardState = (widgets: Widget[] = []): DashboardState => ({
Expand Down Expand Up @@ -173,4 +173,35 @@ describe('move', () => {
])
);
});

it('does not move widget group out of the grid', () => {
const widget1 = MockWidgetFactory.getKpiWidget({ x: 0, y: 0, width: 5, height: 5 });
const widget2 = MockWidgetFactory.getKpiWidget({ x: 5, y: 5, width: 5, height: 5 });

const dashboardState = setupDashboardState([widget1, widget2]);
expect(
moveWidgets(
dashboardState,
onMoveWidgetsAction({
widgets: [widget1, widget2],
vector: { x: dashboardState.grid.width, y: dashboardState.grid.height },
})
).dashboardConfiguration.widgets
).toEqual(
expect.arrayContaining([
expect.objectContaining({
x: 90,
y: 90,
width: 5,
height: 5,
}),
expect.objectContaining({
x: 95,
y: 95,
width: 5,
height: 5,
}),
])
);
});
});
38 changes: 22 additions & 16 deletions packages/dashboard/src/store/actions/moveWidgets/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { constrainWidgetPositionToGrid } from '~/util/constrainWidgetPositionToGrid';
import { trimRectPosition } from '~/util/trimRectPosition';
import type { Action } from 'redux';
import type { Position, Widget } from '~/types';
import type { DashboardState } from '../../state';
import { getSelectionBox } from '~/util/getSelectionBox';
import { moveSelectionBox } from '~/util/moveSelectionBox';
import { transformWidget } from '~/util/transformWidget';

type MoveWidgetsActionPayload = {
widgets: Widget[];
Expand All @@ -21,28 +23,32 @@ export const onMoveWidgetsAction = (payload: MoveWidgetsActionPayload): MoveWidg
});

export const moveWidgets = (state: DashboardState, action: MoveWidgetsAction): DashboardState => {
const vector = action.payload.vector;
const widgets = state.dashboardConfiguration.widgets;
const { vector, complete, widgets } = action.payload;
const selectedWidgetIds = action.payload.widgets.map((w) => w.id);
const movedWidgets = widgets.map((w) => {
if (!selectedWidgetIds.includes(w.id)) {
return w;
}

const widget = { ...w, x: w.x + vector.x, y: w.y + vector.y };
const constrainedWidget = constrainWidgetPositionToGrid(
{ x: 0, y: 0, width: state.grid.width, height: state.grid.height },
widget
);
return action.payload.complete ? trimRectPosition(constrainedWidget) : constrainedWidget;
const selectionBox = getSelectionBox(widgets);
if (!selectionBox) return state;

const newSelectionBox = moveSelectionBox({
selectionBox,
vector,
grid: state.grid,
});

const mover = (widget: Widget) =>
transformWidget(widget, selectionBox, complete ? trimRectPosition(newSelectionBox) : newSelectionBox);

const updateWidgets = (widgets: Widget[]) =>
widgets.map((widget) => {
if (!selectedWidgetIds.includes(widget.id)) return widget;
return mover(widget);
});

return {
...state,
dashboardConfiguration: {
...state.dashboardConfiguration,
widgets: movedWidgets,
widgets: updateWidgets(state.dashboardConfiguration.widgets),
},
selectedWidgets: movedWidgets.filter((w) => selectedWidgetIds.includes(w.id)),
selectedWidgets: updateWidgets(state.selectedWidgets),
};
};
22 changes: 9 additions & 13 deletions packages/dashboard/src/store/actions/resizeWidgets/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { constrainWidgetPositionToGrid } from '~/util/constrainWidgetPositionToGrid';
import { getSelectionBox } from '~/util/getSelectionBox';
import { trimRectPosition } from '~/util/trimRectPosition';
import type { Action } from 'redux';
import type { Position, Widget } from '~/types';
import type { DashboardState } from '../../state';
import { resizeWidget } from '~/util/resizeWidget';
import { transformWidget } from '~/util/transformWidget';
import { resizeSelectionBox } from '~/util/resizeSelectionBox';

export type Anchor = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'left' | 'right' | 'top' | 'bottom';

type ResizeWidgetsActionPayload = {
anchor: Anchor;
widgets: Widget[];
Expand All @@ -35,17 +33,15 @@ export const resizeWidgets = (state: DashboardState, action: ResizeWidgetsAction

if (!selectionBox) return state;

const newSelectionBox = constrainWidgetPositionToGrid(
{
x: 0,
y: 0,
width: state.grid.width,
height: state.grid.height,
},
resizeSelectionBox({ selectionBox, anchor, vector })
);
const newSelectionBox = resizeSelectionBox({
selectionBox,
anchor,
vector,
grid: state.grid,
});

const resizer = (widget: Widget) =>
resizeWidget(widget, selectionBox, complete ? trimRectPosition(newSelectionBox) : newSelectionBox);
transformWidget(widget, selectionBox, complete ? trimRectPosition(newSelectionBox) : newSelectionBox);

const updateWidgets = (widgets: Widget[]) =>
widgets.map((widget) => {
Expand Down
26 changes: 26 additions & 0 deletions packages/dashboard/src/util/moveSelectionBox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { moveSelectionBox } from '~/util/moveSelectionBox';

import { DashboardState } from '~/store/state';

const grid = {
width: 100,
height: 100,
cellSize: 1,
} as DashboardState['grid'];
describe('moveSelectionBox', () => {
const widget1 = { x: 0, y: 0, width: 10, height: 10 };

it('should move selection box', () => {
const selectionBox = widget1;
const vector = { x: 10, y: 10 };
const expected = { x: 10, y: 10, width: 10, height: 10 };
expect(moveSelectionBox({ selectionBox, vector, grid })).toEqual(expected);
});

it('should not move selection box if it is out of bounds', () => {
const selectionBox = widget1;
const vector = { x: 100, y: 100 };
const expected = { x: 90, y: 90, width: 10, height: 10 };
expect(moveSelectionBox({ selectionBox, vector, grid })).toEqual(expected);
});
});
30 changes: 30 additions & 0 deletions packages/dashboard/src/util/moveSelectionBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Position, Rect } from '~/types';
import { DashboardState } from '~/store/state';

export const moveSelectionBox: (params: {
selectionBox: Rect;
vector: Position;
grid: DashboardState['grid'];
}) => Rect = ({ selectionBox, vector, grid }) => {
const newRect = { ...selectionBox };
if (newRect.x + vector.x < 0) {
vector.x = 0 - newRect.x;
}

if (newRect.x + newRect.width + vector.x > grid.width) {
vector.x = grid.width - newRect.x - newRect.width;
}

if (newRect.y + vector.y < 0) {
vector.y = 0 - newRect.y;
}

if (newRect.y + newRect.height + vector.y > grid.height) {
vector.y = grid.height - newRect.y - newRect.height;
}

newRect.x += vector.x;
newRect.y += vector.y;

return newRect;
};

0 comments on commit ea2b875

Please sign in to comment.