feat: Enhance draggable element with multi-sensor support for improved user interaction

This commit is contained in:
jarvis2f 2025-02-19 15:50:35 +08:00
parent 9f37fe8636
commit 7192e1bf2f

View file

@ -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>