✨ 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 React, { useState } from "react";
|
||||||
import { DndContext, type DragEndEvent, useDraggable } from "@dnd-kit/core";
|
import {
|
||||||
|
DndContext,
|
||||||
|
type DragEndEvent,
|
||||||
|
KeyboardSensor,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useDraggable,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
import { Portal } from "@radix-ui/react-portal";
|
import { Portal } from "@radix-ui/react-portal";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface DraggableElementProps {
|
interface DraggableElementProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
onTap?: () => void;
|
|
||||||
longPressTime?: number; // 长按触发拖拽的时间 (ms)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DraggableContent = ({
|
const DraggableContent = ({
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
style,
|
style,
|
||||||
onTap,
|
|
||||||
longPressTime = 300,
|
|
||||||
}: DraggableElementProps & { style: React.CSSProperties }) => {
|
}: DraggableElementProps & { style: React.CSSProperties }) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
||||||
useDraggable({
|
useDraggable({
|
||||||
id: "draggable-element",
|
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
|
const transformStyle = transform
|
||||||
? {
|
? {
|
||||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
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) => {
|
const preventContextMenu = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -90,12 +41,12 @@ const DraggableContent = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
{...(isLongPress ? listeners : {})}
|
{...listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
className={cn(
|
className={cn(
|
||||||
`fixed touch-none select-none`,
|
`fixed touch-none select-none`,
|
||||||
isDragging && "opacity-80",
|
isDragging && "opacity-80",
|
||||||
isLongPress && "cursor-grabbing",
|
isDragging && "cursor-grabbing",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -107,16 +58,13 @@ const DraggableContent = ({
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
onContextMenu={preventContextMenu}
|
onContextMenu={preventContextMenu}
|
||||||
onTouchStart={handleTouchStart}
|
|
||||||
onTouchEnd={handleTouchEnd}
|
|
||||||
onTouchMove={handleTouchMove}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative ${isLongPress ? "scale-105 transition-transform" : ""}`}
|
className={`relative ${isDragging ? "scale-105 transition-transform" : ""}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{isLongPress && (
|
{isDragging && (
|
||||||
<div className="absolute inset-0 animate-pulse rounded-lg bg-black/5" />
|
<div className="absolute inset-0 animate-pulse rounded-lg bg-black/5" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,6 +77,21 @@ const DraggableElement = (props: DraggableElementProps) => {
|
||||||
y: window.innerHeight - 60,
|
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 handleDragEnd = (event: DragEndEvent) => {
|
||||||
const { delta } = event;
|
const { delta } = event;
|
||||||
|
|
||||||
|
|
@ -152,7 +115,7 @@ const DraggableElement = (props: DraggableElementProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<DndContext onDragEnd={handleDragEnd}>
|
<DndContext onDragEnd={handleDragEnd} sensors={sensors}>
|
||||||
<div id="draggable-content">
|
<div id="draggable-content">
|
||||||
<DraggableContent {...props} style={style} />
|
<DraggableContent {...props} style={style} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue