如何让React自定义Draggable组件的onDrop事件生效(兼容触摸与鼠标)
问题分析
你遇到的核心问题是:原生HTML5的onDrop事件只属于原生拖拽API体系,只有当元素开启draggable="true"并遵循原生拖拽流程(dragstart/dragover等)时才会触发。而你现在是用鼠标/触摸事件手动实现的拖拽逻辑,完全独立于原生拖拽API,所以容器上的onDrop自然不会被触发。
解决方案
既然不用原生draggable属性,我们需要手动模拟drop逻辑:在拖拽结束(mouseup/touchend)时,获取当前鼠标/触摸点的位置,判断该点是否落在目标Drapable容器内,然后手动执行元素重排的逻辑。
以下是修改后的完整代码,关键改动已标注:
import { useEffect, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useRef, useState, ReactElement } from "react" interface IDraggableProps { children: ReactElement[] | ReactElement | string; item: IItem; // 新增:传递当前拖拽的item数据 onDragEnd: (position: {x: number, y: number}) => void; // 新增:拖拽结束时通知父组件落点 } const Draggable = (props: IDraggableProps) => { const [isDragging, setIsDragging] = useState(false); const [xTranslate, setXTranslate] = useState(0); const [yTranslate, setYTranslate] = useState(0); const [initialMousePosition, setInitialMousePosition] = useState({ x: 0, y: 0 }); const initialWidth = useRef<number>(0); const onMouseDown = (e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => { setInitialMousePosition({ x: e.clientX, y: e.clientY }); setIsDragging(true); }; const onTouchStart = (e: ReactTouchEvent<HTMLDivElement>) => { setInitialMousePosition({ x: e.touches[0].clientX, y: e.touches[0].clientY }); setIsDragging(true); e.preventDefault(); // 新增:阻止触摸默认行为,避免页面滚动干扰 }; const onMouseMove = (e: MouseEvent) => { if (!isDragging) return; setXTranslate(prev => prev + e.clientX - initialMousePosition.x); setYTranslate(prev => prev + e.clientY - initialMousePosition.y); setInitialMousePosition({x: e.clientX, y: e.clientY}); // 修复:更新初始位置,避免位移跳变 }; const onTouchMove = (e: TouchEvent) => { if (!isDragging) return; const touch = e.touches[0]; setXTranslate(prev => prev + touch.clientX - initialMousePosition.x); setYTranslate(prev => prev + touch.clientY - initialMousePosition.y); setInitialMousePosition({x: touch.clientX, y: touch.clientY}); // 修复:同上 e.preventDefault(); // 新增:阻止触摸滚动 }; // 拖拽结束处理 const handleDragEnd = (clientX: number, clientY: number) => { setIsDragging(false); // 通知父组件当前落点位置 props.onDragEnd({x: clientX, y: clientY}); }; useEffect(() => { if (isDragging) { window.addEventListener("mousemove", onMouseMove); window.addEventListener("touchmove", onTouchMove); } return () => { window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("touchmove", onTouchMove); } }, [isDragging, initialMousePosition]); useEffect(() => { const onMouseUp = (e: MouseEvent) => handleDragEnd(e.clientX, e.clientY); const onTouchEnd = (e: TouchEvent) => { if (e.changedTouches[0]) { handleDragEnd(e.changedTouches[0].clientX, e.changedTouches[0].clientY); } }; window.addEventListener("mouseup", onMouseUp); window.addEventListener("touchend", onTouchEnd); return () => { window.removeEventListener("mouseup", onMouseUp); window.removeEventListener("touchend", onTouchEnd); } }, []); useEffect(() => { if (!isDragging) { // 修复:只有拖拽结束时重置位移 setXTranslate(0); setYTranslate(0); } }, [isDragging]) return ( <div className="relative"> <div style={isDragging ? { width: initialWidth.current + "px", transform: `translate(${xTranslate}px,${yTranslate}px)`, position: "absolute", zIndex: 100, // 新增:拖拽时提升层级,避免被遮挡 pointerEvents: "none" // 新增:拖拽时禁用自身点击事件,避免干扰落点判断 } : undefined} onMouseDown={onMouseDown} onTouchStart={onTouchStart} ref={(e) => initialWidth.current = e?.offsetWidth!} className={`${isDragging ? "cursor-grabbing" : "transition-all cursor-grab"} static w-full flex justify-center items-center text-xl font-bold text-primary p-2 shadow-md border bg-slate-50`}> {props.children} </div> </div> ); } interface IItem { data: string; row: number; id: string; // 新增:给每个item添加唯一ID,避免map用index做key的问题 } const MyTasks = () => { const [data, setData] = useState<IItem[]>([ { id: "item-1", row: 1, data: "item-1" }, { id: "item-2", row: 1, data: "item-2" }, { id: "item-3", row: 1, data: "item-3" }, { id: "item-4", row: 1, data: "item-4" }, { id: "item-5", row: 2, data: "item-5" }, { id: "item-6", row: 2, data: "item-6" }, { id: "item-7", row: 1, data: "item-7" } ]); const [draggingItem, setDraggingItem] = useState<IItem | null>(null); // 存储所有Drapable容器的ref和对应的row const containerRefs = useRef<Map<number, HTMLDivElement>>(new Map()); // 处理拖拽结束逻辑 const handleDragEnd = (position: {x: number, y: number}, item: IItem) => { if (!draggingItem) return; // 遍历所有容器,判断落点在哪个容器内 for (const [row, container] of containerRefs.current.entries()) { const rect = container.getBoundingClientRect(); const isInContainer = position.x >= rect.left && position.x <= rect.right && position.y >= rect.top && position.y <= rect.bottom; if (isInContainer && row !== item.row) { // 更新item的row,实现重排 setData(prev => prev.map(i => i.id === item.id ? {...i, row} : i )); break; } } setDraggingItem(null); }; return ( <div className="flex prevent-select flex-row justify-between bg-white shadow-md p-4 gap-4"> {[1,2,3].map(row => ( <Drapable key={row} items={data} row={row} containerRef={(el) => el && containerRefs.current.set(row, el)} onItemDragStart={(item) => setDraggingItem(item)} /> ))} </div> ) } interface IDrapableProps { items: IItem[]; row: number; containerRef: (el: HTMLDivElement | null) => void; // 新增:传递容器ref给父组件 onItemDragStart: (item: IItem) => void; // 新增:通知父组件开始拖拽的item } const Drapable = (props: IDrapableProps) => { return ( <div className="flex flex-col bg-white rounded-md p-2 shadow-md gap-2 border border-primary w-full h-[500px]"> <h2 className="text-2xl font-bold text-center text-secondary">row-{props.row}</h2> <div className="flex gap-2 flex-col w-full h-full p-2" ref={props.containerRef} // 绑定容器ref > {props.items.map((item) => { if (item.row === props.row) return ( <Draggable key={item.id} // 用唯一ID做key item={item} onDragEnd={(pos) => handleDragEnd(pos, item)} > <p>{item.data}</p> </Draggable> ) return null; })} </div> </div> ) } export default MyTasks;
关键改动说明
状态与通信优化:
- 给每个
IItem添加唯一id,避免用数组index作为key导致的渲染问题 - 在
MyTasks中维护draggingItem状态,记录当前正在拖拽的元素 - 通过回调函数在
Draggable和Drapable、MyTasks之间传递拖拽状态和位置信息
- 给每个
拖拽逻辑修复:
- 修复了鼠标/触摸移动时的位移计算问题(之前的逻辑会导致位移跳变)
- 触摸事件中添加
e.preventDefault(),阻止页面滚动干扰拖拽 - 拖拽时给元素添加
z-index提升层级,pointerEvents: none避免自身遮挡落点判断
手动模拟Drop逻辑:
- 给每个
Drapable容器绑定ref,存储到containerRefs中 - 在拖拽结束时,通过
getBoundingClientRect()获取容器的位置和尺寸,判断落点是否在容器内 - 如果落点在目标容器,更新对应item的
row属性,实现元素重排
- 给每个
内容的提问来源于stack exchange,提问作者noone
相关产品推荐
相关产品推荐

