Wrote drag and drop hook and tests.
This commit is contained in:
+241
-34
@@ -1,52 +1,259 @@
|
|||||||
import { useState, useRef, useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useReducer, useRef } from "react";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
|
||||||
type Position = {
|
import {
|
||||||
x: number;
|
extractEventCoordinates,
|
||||||
y: number;
|
isLeftMouseButton,
|
||||||
};
|
isTouchEvent,
|
||||||
|
} from "../util";
|
||||||
|
|
||||||
type DragAndDropConfig = {
|
type DragAction<T> =
|
||||||
longPressEnabled?: boolean;
|
| { type: "resetItems"; items: T[] }
|
||||||
longPressDelay?: number;
|
| { type: "startDrag"; sourceIndex: number; items: T[] }
|
||||||
dragHandleSelector?: string;
|
| { type: "processMove"; cursor: { x: number; y: number }; rects: DOMRect[] }
|
||||||
};
|
| { type: "endDrag"; handleReorder: (newItems: T[]) => void };
|
||||||
|
|
||||||
type DragAndDropState = {
|
interface DragState<T> {
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
currentIndex: number | null;
|
sourceIndex: number;
|
||||||
targetIndex: number | null;
|
targetIndex: number;
|
||||||
|
items: T[];
|
||||||
|
previewItems: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer<T>(state: DragState<T>, action: DragAction<T>) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "resetItems":
|
||||||
|
const items = action.items;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: [...items],
|
||||||
|
previewItems: [...items],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useDragAndDrop<T>(
|
case "startDrag":
|
||||||
items: T[],
|
return {
|
||||||
onReorder: (newOrder: T[]) => void,
|
...state,
|
||||||
config: DragAndDropConfig = {},
|
isDragging: true,
|
||||||
|
sourceIndex: action.sourceIndex,
|
||||||
|
targetIndex: action.sourceIndex,
|
||||||
|
items: [...action.items],
|
||||||
|
previewItems: [...action.items],
|
||||||
|
};
|
||||||
|
|
||||||
|
case "processMove":
|
||||||
|
if (!state.isDragging) return state;
|
||||||
|
|
||||||
|
const { cursor, rects } = action;
|
||||||
|
let newTargetIndex = state.targetIndex;
|
||||||
|
|
||||||
|
for (let i = 0; i < rects.length; i++) {
|
||||||
|
const rect = rects[i];
|
||||||
|
if (
|
||||||
|
cursor.x >= rect.left &&
|
||||||
|
cursor.x <= rect.right &&
|
||||||
|
cursor.y >= rect.top &&
|
||||||
|
cursor.y <= rect.bottom
|
||||||
) {
|
) {
|
||||||
const {
|
newTargetIndex = i;
|
||||||
longPressEnabled = false,
|
break;
|
||||||
longPressDelay = 300,
|
}
|
||||||
dragHandleSelector,
|
}
|
||||||
} = config;
|
|
||||||
|
|
||||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
if (newTargetIndex === state.targetIndex) return state;
|
||||||
const containerRef = useRef<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const [dragState, setDragState] = useState<DragAndDropState>({
|
const newPreviewItems = [...state.items];
|
||||||
|
const [movedItem] = newPreviewItems.splice(state.sourceIndex, 1);
|
||||||
|
newPreviewItems.splice(newTargetIndex, 0, movedItem);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
targetIndex: newTargetIndex,
|
||||||
|
previewItems: newPreviewItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
case "endDrag":
|
||||||
|
if (state.sourceIndex !== state.targetIndex) {
|
||||||
|
action.handleReorder(state.previewItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
currentIndex: null,
|
sourceIndex: -1,
|
||||||
targetIndex: null,
|
targetIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DragAndDropHook<T> {
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
getItemRef: (id: string) => RefObject<HTMLElement | null>;
|
||||||
|
setItemRef: (el: HTMLElement | null, id: string) => void;
|
||||||
|
isDragging: boolean;
|
||||||
|
sourceIndex: number;
|
||||||
|
targetIndex: number;
|
||||||
|
previewItems: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemRefMap = {
|
||||||
|
[key: string]: RefObject<HTMLElement | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDragAndDrop<T extends { id: string }>({
|
||||||
|
items,
|
||||||
|
handleReorder,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
items: T[];
|
||||||
|
handleReorder: (newItems: T[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}): DragAndDropHook<T> {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const { itemRefs, getItemRef, setItemRef } = useItemRefs<T>(items);
|
||||||
|
const itemBoundingRects = useRef<DOMRect[]>([]);
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
isDragging: false,
|
||||||
|
sourceIndex: -1,
|
||||||
|
targetIndex: -1,
|
||||||
|
items: [...items],
|
||||||
|
previewItems: [...items],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getItemProps = (index: number) => {
|
// Set preview items when 'items' changes externally
|
||||||
return { style: {} };
|
useEffect(() => {
|
||||||
|
dispatch({ type: "resetItems", items });
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
// Event Handlers
|
||||||
|
function getItemElement(event: MouseEvent | TouchEvent) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const itemElement = target.closest("[data-item-id]") as HTMLElement | null;
|
||||||
|
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemIndex(el: HTMLElement) {
|
||||||
|
const itemId = el.dataset.itemId;
|
||||||
|
const index = items.findIndex((item) => item.id === itemId);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureItemBoundaries() {
|
||||||
|
itemBoundingRects.current = items.map((item) => {
|
||||||
|
const el = itemRefs.current[item.id]?.current;
|
||||||
|
return el ? el.getBoundingClientRect() : new DOMRect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragMove = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { clientX, clientY } = extractEventCoordinates(event);
|
||||||
|
dispatch({
|
||||||
|
type: "processMove",
|
||||||
|
cursor: { x: clientX, y: clientY },
|
||||||
|
rects: itemBoundingRects.current,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
document.removeEventListener("mousemove", handleDragMove);
|
||||||
|
document.removeEventListener("touchmove", handleDragMove);
|
||||||
|
document.removeEventListener("mouseup", handleDragEnd);
|
||||||
|
document.removeEventListener("touchend", handleDragEnd);
|
||||||
|
document.removeEventListener("touchcancel", handleDragEnd);
|
||||||
|
|
||||||
|
dispatch({ type: "endDrag", handleReorder });
|
||||||
|
}, [dispatch, handleDragMove, handleReorder]);
|
||||||
|
|
||||||
|
const handleDragStart = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent) => {
|
||||||
|
if (!isTouchEvent(event) && !isLeftMouseButton(event.buttons)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const itemElement = getItemElement(event);
|
||||||
|
if (itemElement) {
|
||||||
|
const sourceIndex = getItemIndex(itemElement);
|
||||||
|
if (sourceIndex !== -1) {
|
||||||
|
captureItemBoundaries();
|
||||||
|
dispatch({ type: "startDrag", sourceIndex, items });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleDragMove);
|
||||||
|
document.addEventListener("touchmove", handleDragMove);
|
||||||
|
document.addEventListener("mouseup", handleDragEnd);
|
||||||
|
document.addEventListener("touchend", handleDragEnd);
|
||||||
|
document.addEventListener("touchcancel", handleDragEnd);
|
||||||
|
},
|
||||||
|
[items, dispatch, handleDragEnd, handleDragMove],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set/cleanup event handlers
|
||||||
|
useEffect(() => {
|
||||||
|
const elements = Object.values(itemRefs.current)
|
||||||
|
.map((ref) => ref.current)
|
||||||
|
.filter((el) => el !== null);
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (el) {
|
||||||
|
el.addEventListener("mousedown", handleDragStart);
|
||||||
|
el.addEventListener("touchstart", handleDragStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
elements.forEach((el) => {
|
||||||
|
if (el) {
|
||||||
|
el.removeEventListener("mousedown", handleDragStart);
|
||||||
|
el.removeEventListener("touchstart", handleDragStart);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}, [items, handleDragStart, disabled]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
containerRef,
|
containerRef,
|
||||||
itemRefs,
|
getItemRef,
|
||||||
isDragging: dragState.isDragging,
|
setItemRef,
|
||||||
currentIndex: dragState.currentIndex,
|
isDragging: state.isDragging,
|
||||||
targetIndex: dragState.targetIndex,
|
sourceIndex: state.sourceIndex,
|
||||||
getItemProps,
|
targetIndex: state.targetIndex,
|
||||||
|
previewItems: state.previewItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useItemRefs<T extends { id: string }>(items: T[]) {
|
||||||
|
const itemRefs = useRef<ItemRefMap>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
itemRefs.current = items.reduce((acc, item) => {
|
||||||
|
acc[item.id] = itemRefs.current[item.id] || { current: null };
|
||||||
|
return acc;
|
||||||
|
}, {} as ItemRefMap);
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const getItemRef = useCallback((id: string) => {
|
||||||
|
return itemRefs.current[id] || { current: null };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setItemRef = useCallback((el: HTMLElement | null, id: string) => {
|
||||||
|
if (!itemRefs.current[id]) {
|
||||||
|
itemRefs.current[id] = { current: null };
|
||||||
|
}
|
||||||
|
itemRefs.current[id].current = el;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { itemRefs, getItemRef, setItemRef };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { RefObject } from "react";
|
|
||||||
import { useDragAndDrop } from "../dragAndDrop";
|
import { useDragAndDrop } from "../dragAndDrop";
|
||||||
import styles from "./dragAndDropTest.module.css";
|
import styles from "./dragAndDropTest.module.css";
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
type Item = {
|
type Item = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,87 +12,83 @@ type Item = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initialItems: Item[] = [
|
const initialItems: Item[] = [
|
||||||
{ id: "1", content: "Item 1", color: "firebrick" },
|
{ id: "A", content: "Item A", color: "firebrick" },
|
||||||
{ id: "2", content: "Item 2", color: "mediumseagreen" },
|
{ id: "B", content: "Item B", color: "mediumseagreen" },
|
||||||
{ id: "3", content: "Item 3", color: "orange" },
|
{ id: "C", content: "Item C", color: "orange" },
|
||||||
{ id: "4", content: "Item 4", color: "deepskyblue" },
|
{ id: "D", content: "Item D", color: "deepskyblue" },
|
||||||
{ id: "5", content: "Item 5", color: "mediumorchid" },
|
{ id: "E", content: "Item E", color: "mediumorchid" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function TestDragAndDrop() {
|
function TestDragAndDrop() {
|
||||||
const [items, setItems] = useState<Item[]>(initialItems);
|
const [items, setItems] = useState<Item[]>(initialItems);
|
||||||
const [useLongPress, setUseLongPress] = useState(false);
|
const [dragEnabled, setDragEnabled] = useState(true);
|
||||||
const [useDragHandle, setUseDragHandle] = useState(false);
|
|
||||||
|
|
||||||
const handleReorder = (newItems: Item[]) => {
|
const handleReorder = (newItems: Item[]) => setItems(newItems);
|
||||||
setItems(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { containerRef, getItemProps, isDragging, currentIndex, targetIndex } =
|
const {
|
||||||
useDragAndDrop(items, handleReorder, {
|
containerRef,
|
||||||
longPressEnabled: useLongPress,
|
setItemRef,
|
||||||
dragHandleSelector: useDragHandle ? ".drag-handle" : undefined,
|
isDragging,
|
||||||
|
sourceIndex,
|
||||||
|
targetIndex,
|
||||||
|
previewItems,
|
||||||
|
} = useDragAndDrop({
|
||||||
|
items,
|
||||||
|
handleReorder,
|
||||||
|
disabled: !dragEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div
|
<div className={styles.itemsWrapper}>
|
||||||
ref={containerRef as RefObject<HTMLDivElement>}
|
<div className={styles.indexColumn}>
|
||||||
className={styles.itemsWrapper}
|
{(isDragging ? previewItems : items).map((_, index) => (
|
||||||
>
|
<div key={`index-${index}`} className={styles.indexItem}>
|
||||||
{items.map((item, index) => (
|
{index}
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
data-cy={`draggable-item-${item.id}`}
|
|
||||||
{...getItemProps(index)}
|
|
||||||
className={clsx(
|
|
||||||
styles.draggableItem,
|
|
||||||
isDragging && currentIndex === index && styles.dragging,
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
...getItemProps(index).style,
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className={styles.content}>{item.content}</span>
|
|
||||||
{useDragHandle && <div className={styles.handle}></div>}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<div ref={containerRef} className={styles.draggableItemsWrapper}>
|
||||||
|
{(isDragging ? previewItems : items).map((item, index) => (
|
||||||
<div className={styles.controls}>
|
<motion.div
|
||||||
<label>
|
key={item.id}
|
||||||
<input
|
ref={(el) => setItemRef(el, item.id)}
|
||||||
type="checkbox"
|
data-item-id={item.id}
|
||||||
checked={useLongPress}
|
data-cy={`draggable-item-${item.id}`}
|
||||||
onChange={() => setUseLongPress(!useLongPress)}
|
className={clsx(styles.item, {
|
||||||
/>
|
[styles.draggableItem]: dragEnabled,
|
||||||
Use Long Press
|
[styles.draggingItem]: isDragging && targetIndex === index,
|
||||||
</label>
|
})}
|
||||||
|
style={{
|
||||||
<label>
|
backgroundColor: item.color,
|
||||||
<input
|
}}
|
||||||
type="checkbox"
|
layout
|
||||||
checked={useDragHandle}
|
transition={{ duration: 0.2 }}
|
||||||
onChange={() => setUseDragHandle(!useDragHandle)}
|
>
|
||||||
/>
|
<span className={styles.content}>{item.content}</span>
|
||||||
Use Drag Handle
|
</motion.div>
|
||||||
</label>
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div className={styles.status}>
|
<div className={styles.status}>
|
||||||
|
<button
|
||||||
|
onClick={() => setDragEnabled(!dragEnabled)}
|
||||||
|
data-cy="enable-button"
|
||||||
|
>
|
||||||
|
{dragEnabled ? "Cancel" : "Reorder Items"}
|
||||||
|
</button>
|
||||||
<p>Dragging: {isDragging ? "True" : "False"}</p>
|
<p>Dragging: {isDragging ? "True" : "False"}</p>
|
||||||
<p>
|
<p>
|
||||||
Status:
|
Status:{" "}
|
||||||
{isDragging && (
|
{isDragging && (
|
||||||
<p>
|
<>
|
||||||
Moving Item {currentIndex !== null ? items[currentIndex].id : ""}
|
Moving Item {sourceIndex !== null ? items[sourceIndex].id : ""}
|
||||||
{targetIndex !== null ? ` to position ${targetIndex}` : ""}
|
{targetIndex !== null ? ` to position ${targetIndex}` : ""}
|
||||||
</p>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,24 +96,127 @@ function TestDragAndDrop() {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div className={styles.currentOrder}>
|
<div className={styles.currentOrder}>
|
||||||
<p>Current order:</p>
|
<p>Item order: {items.map((i) => i.id)}</p>
|
||||||
<pre>
|
<p>Preview order: {previewItems.map((i) => i.id)}</p>
|
||||||
{JSON.stringify(
|
|
||||||
items.map((i) => i.id),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerMouseEvent = (
|
||||||
|
testId: string,
|
||||||
|
eventType: string,
|
||||||
|
args: any[] = [],
|
||||||
|
opts: { [key: string]: any } = {},
|
||||||
|
) => {
|
||||||
|
cy.dataCy(testId).trigger(eventType, ...args, {
|
||||||
|
buttons: 1,
|
||||||
|
eventConstructor: "MouseEvent",
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerTouchEvent = (
|
||||||
|
testId: string,
|
||||||
|
eventType: string,
|
||||||
|
args: any[] = [],
|
||||||
|
opts: { [key: string]: any } = {},
|
||||||
|
) => {
|
||||||
|
cy.dataCy(testId).then(($el) => {
|
||||||
|
const { left, top, width, height } = $el[0].getBoundingClientRect();
|
||||||
|
const centerX = left + width / 2;
|
||||||
|
const centerY = top + height / 2;
|
||||||
|
|
||||||
|
const clientX = opts.clientX || centerX;
|
||||||
|
const clientY = opts.clientY || centerY;
|
||||||
|
|
||||||
|
const touchList = [
|
||||||
|
{
|
||||||
|
identifier: 0,
|
||||||
|
target: $el[0],
|
||||||
|
clientX,
|
||||||
|
clientY,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const touchOptions = {
|
||||||
|
...opts,
|
||||||
|
touches: touchList,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cy.dataCy(testId).trigger(eventType, ...args, touchOptions);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertItemOrder = (expectedOrder: string) => {
|
||||||
|
cy.contains(`Item order: ${expectedOrder}`).should("exist");
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertPreviewOrder = (expectedOrder: string) => {
|
||||||
|
cy.contains(`Preview order: ${expectedOrder}`).should("exist");
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertDragState = (expectedState: boolean) => {
|
||||||
|
const expectedValue = expectedState ? "True" : "False";
|
||||||
|
cy.contains(`Dragging: ${expectedValue}`).should("exist");
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertStatus = (expectedItem: string, expectedPosition: string) => {
|
||||||
|
cy.contains(
|
||||||
|
`Status: Moving ${expectedItem} to position ${expectedPosition}`,
|
||||||
|
).should("exist");
|
||||||
|
};
|
||||||
|
|
||||||
describe("Drag and Drop Component Test", () => {
|
describe("Drag and Drop Component Test", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.mount(<TestDragAndDrop />);
|
cy.mount(<TestDragAndDrop />);
|
||||||
cy.viewport(500, 600);
|
cy.viewport(500, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should drag and drop", () => {});
|
it("should drag and drop on mouse events", () => {
|
||||||
|
assertItemOrder("ABCDE");
|
||||||
|
assertPreviewOrder("ABCDE");
|
||||||
|
assertDragState(false);
|
||||||
|
|
||||||
|
triggerMouseEvent("draggable-item-A", "mousedown");
|
||||||
|
assertDragState(true);
|
||||||
|
assertStatus("Item A", "0");
|
||||||
|
|
||||||
|
triggerMouseEvent("draggable-item-B", "mousemove");
|
||||||
|
assertStatus("Item A", "1");
|
||||||
|
assertItemOrder("ABCDE");
|
||||||
|
assertPreviewOrder("BACDE");
|
||||||
|
|
||||||
|
triggerMouseEvent("draggable-item-A", "mouseup");
|
||||||
|
assertDragState(false);
|
||||||
|
assertItemOrder("BACDE");
|
||||||
|
assertPreviewOrder("BACDE");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should drag and drop on touch events", () => {
|
||||||
|
assertItemOrder("ABCDE");
|
||||||
|
assertPreviewOrder("ABCDE");
|
||||||
|
assertDragState(false);
|
||||||
|
|
||||||
|
triggerTouchEvent("draggable-item-A", "touchstart");
|
||||||
|
assertDragState(true);
|
||||||
|
assertStatus("Item A", "0");
|
||||||
|
|
||||||
|
triggerTouchEvent("draggable-item-B", "touchmove");
|
||||||
|
assertStatus("Item A", "1");
|
||||||
|
assertItemOrder("ABCDE");
|
||||||
|
assertPreviewOrder("BACDE");
|
||||||
|
|
||||||
|
triggerTouchEvent("draggable-item-A", "touchend");
|
||||||
|
assertDragState(false);
|
||||||
|
assertItemOrder("BACDE");
|
||||||
|
assertPreviewOrder("BACDE");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should disable dragging", () => {
|
||||||
|
cy.dataCy("enable-button").click();
|
||||||
|
triggerMouseEvent("draggable-item-A", "mousedown");
|
||||||
|
assertDragState(false);
|
||||||
|
triggerMouseEvent("draggable-item-A", "mouseup");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,22 +2,57 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.itemsWrapper {
|
.itemsWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableItem {
|
.indexColumn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indexItem {
|
||||||
|
width: 40px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #eee;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggableItemsWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableItem .content {
|
.draggableItem {
|
||||||
flex: 1;
|
width: 196px;
|
||||||
|
height: 46px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggingItem {
|
||||||
|
width: 204px;
|
||||||
|
height: 54px;
|
||||||
|
margin: -2px;
|
||||||
|
box-shadow: 0px 0px 10px #777;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .content {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableItem .handle {
|
.draggableItem .grip {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
@@ -34,6 +69,7 @@
|
|||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
|
margin: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
|
|||||||
Reference in New Issue
Block a user