✨ feat: Enhance draggable element with multi-sensor support for improved user interaction
This commit is contained in:
parent
9f37fe8636
commit
7192e1bf2f
1 changed files with 31 additions and 68 deletions
|
|
@ -1,87 +1,38 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { DndContext, type DragEndEvent, useDraggable } from "@dnd-kit/core";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useDraggable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { Portal } from "@radix-ui/react-portal";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface DraggableElementProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onTap?: () => void;
|
||||
longPressTime?: number; // 长按触发拖拽的时间 (ms)
|
||||
}
|
||||
|
||||
const DraggableContent = ({
|
||||
children,
|
||||
className = "",
|
||||
style,
|
||||
onTap,
|
||||
longPressTime = 300,
|
||||
}: DraggableElementProps & { style: React.CSSProperties }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||
useDraggable({
|
||||
id: "draggable-element",
|
||||
});
|
||||
|
||||
const touchStartTime = useRef<number>(0);
|
||||
const touchStartPos = useRef<{ x: number; y: number } | null>(null);
|
||||
const longPressTimer = useRef<NodeJS.Timeout>();
|
||||
const [isLongPress, setIsLongPress] = useState(false);
|
||||
|
||||
const transformStyle = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: {};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
touchStartTime.current = Date.now();
|
||||
touchStartPos.current = {
|
||||
x: e.touches[0]!.clientX,
|
||||
y: e.touches[0]!.clientY,
|
||||
};
|
||||
|
||||
longPressTimer.current = setTimeout(() => {
|
||||
setIsLongPress(true);
|
||||
}, longPressTime);
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||
clearTimeout(longPressTimer.current);
|
||||
|
||||
if (touchStartPos.current) {
|
||||
const touchEndX = e.changedTouches[0]!.clientX;
|
||||
const touchEndY = e.changedTouches[0]!.clientY;
|
||||
const deltaX = Math.abs(touchEndX - touchStartPos.current.x);
|
||||
const deltaY = Math.abs(touchEndY - touchStartPos.current.y);
|
||||
const touchDuration = Date.now() - touchStartTime.current;
|
||||
|
||||
// 如果移动距离小于阈值且不是长按,则视为点击
|
||||
if (
|
||||
deltaX < 5 &&
|
||||
deltaY < 5 &&
|
||||
touchDuration < longPressTime &&
|
||||
!isLongPress
|
||||
) {
|
||||
onTap?.();
|
||||
}
|
||||
}
|
||||
|
||||
setIsLongPress(false);
|
||||
touchStartPos.current = null;
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (touchStartPos.current) {
|
||||
const deltaX = Math.abs(e.touches[0]!.clientX - touchStartPos.current.x);
|
||||
const deltaY = Math.abs(e.touches[0]!.clientY - touchStartPos.current.y);
|
||||
|
||||
// 如果移动距离超过阈值,取消长按计时
|
||||
if (deltaX > 5 || deltaY > 5) {
|
||||
clearTimeout(longPressTimer.current);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preventContextMenu = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -90,12 +41,12 @@ const DraggableContent = ({
|
|||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
{...(isLongPress ? listeners : {})}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className={cn(
|
||||
`fixed touch-none select-none`,
|
||||
isDragging && "opacity-80",
|
||||
isLongPress && "cursor-grabbing",
|
||||
isDragging && "cursor-grabbing",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -107,16 +58,13 @@ const DraggableContent = ({
|
|||
userSelect: "none",
|
||||
}}
|
||||
onContextMenu={preventContextMenu}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchMove={handleTouchMove}
|
||||
>
|
||||
<div
|
||||
className={`relative ${isLongPress ? "scale-105 transition-transform" : ""}`}
|
||||
className={`relative ${isDragging ? "scale-105 transition-transform" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{isLongPress && (
|
||||
{isDragging && (
|
||||
<div className="absolute inset-0 animate-pulse rounded-lg bg-black/5" />
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -129,6 +77,21 @@ const DraggableElement = (props: DraggableElementProps) => {
|
|||
y: window.innerHeight - 60,
|
||||
});
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
});
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
},
|
||||
});
|
||||
const keyboardSensor = useSensor(KeyboardSensor, {});
|
||||
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { delta } = event;
|
||||
|
||||
|
|
@ -152,7 +115,7 @@ const DraggableElement = (props: DraggableElementProps) => {
|
|||
|
||||
return (
|
||||
<Portal>
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<DndContext onDragEnd={handleDragEnd} sensors={sensors}>
|
||||
<div id="draggable-content">
|
||||
<DraggableContent {...props} style={style} />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue