如何在@dnd-kit中限制任务卡片拖拽范围至外层容器内(避免无限滚动)
我太懂你的痛点了——用@dnd-kit做跨列拖拽任务时,卡片能拖到滚动容器外面导致无限滚动,内置的restrictToWindowEdges或者restrictToFirstScrollableAncestor都没达到预期效果对吧?其实问题出在这些内置修饰符可能没有精准识别到你的外层滚动容器,或者边界计算不符合你的布局需求。咱们可以通过自定义拖拽修饰符来解决这个问题,直接把拖拽范围锁死在包裹所有列的容器里,同时不影响列间的正常拖拽。
步骤1:给外层滚动容器添加Ref
首先你需要获取到那个包裹所有列的滚动容器的DOM节点,用useRef来实现。先补充导入并创建ref:
import { useRef } from 'react'; import { Modifier } from '@dnd-kit/core'; // 别忘了引入Modifier类型
然后在组件内部添加ref声明:
const taskContainerRef = useRef<HTMLDivElement>(null);
接着把这个ref绑定到你的外层滚动容器上:
// 给原来的外层div加上ref属性 <div ref={taskContainerRef} className="flex gap-5 overflow-x-scroll 2xl:overflow-auto pb-32" >
步骤2:自定义拖拽范围限制修饰符
接下来写一个自定义的modifier函数,它会读取容器的边界信息,然后限制拖拽元素的位置,不让它超出容器的上下左右边界。
在组件内部添加这个函数:
const restrictToContainer: Modifier = ({ transform, ...rest }) => { if (!taskContainerRef.current) return transform; // 获取容器的绝对边界信息 const containerRect = taskContainerRef.current.getBoundingClientRect(); // 获取当前拖拽元素的尺寸 const dragElementRect = rest.active.node.current?.getBoundingClientRect(); if (!dragElementRect) return transform; // 计算允许的最大/最小拖拽位置,确保卡片完全在容器内 const maxX = containerRect.right - dragElementRect.width; const minX = containerRect.left; const maxY = containerRect.bottom - dragElementRect.height; const minY = containerRect.top; // 将拖拽坐标限制在上述范围内 const clampedX = Math.max(minX, Math.min(maxX, transform.x)); const clampedY = Math.max(minY, Math.min(maxY, transform.y)); return { ...transform, x: clampedX, y: clampedY, }; };
这个逻辑很直观:先拿到容器和拖拽卡片的位置尺寸,然后算出卡片能移动的极限坐标,最后把拖拽的实际位置"夹紧"在这个范围内,这样卡片就再也跑不出容器了。
步骤3:在DndContext中启用自定义修饰符
最后,把这个自定义修饰符加到DndContext的modifiers属性里。如果之前用了内置修饰符,可以替换掉,或者和其他必要的修饰符组合使用:
<DndContext onDragEnd={handleDragEnd} modifiers={[restrictToContainer]} // 启用自定义修饰符 >
比如你如果需要垂直方向的拖拽限制,可以这样组合:
modifiers={[restrictToContainer, restrictToVerticalAxis]}
为什么内置修饰符没生效?
你提到的restrictToFirstScrollableAncestor失效,大概率是因为你的滚动容器不是拖拽元素的直接祖先,或者容器的滚动属性在断点(比如2xl)下切换成了overflow-auto,导致内置修饰符无法正确识别目标容器。自定义修饰符直接绑定到你指定的容器,就不会有这个问题。
修改后的完整关键代码
把上面的修改整合到你的组件里,关键部分如下:
import { useRef } from 'react'; import { DndContext, DragEndEvent, Modifier } from "@dnd-kit/core"; // ...其他导入保持不变 export default function TaskList({ tasks, canEdit }: TasksListProps) { const params = useParams(); const projectId = params.projectId!; const queryClient = useQueryClient(); const taskContainerRef = useRef<HTMLDivElement>(null); // 新增容器ref // 自定义拖拽范围限制修饰符 const restrictToContainer: Modifier = ({ transform, ...rest }) => { if (!taskContainerRef.current) return transform; const containerRect = taskContainerRef.current.getBoundingClientRect(); const dragElementRect = rest.active.node.current?.getBoundingClientRect(); if (!dragElementRect) return transform; const maxX = containerRect.right - dragElementRect.width; const minX = containerRect.left; const maxY = containerRect.bottom - dragElementRect.height; const minY = containerRect.top; const clampedX = Math.max(minX, Math.min(maxX, transform.x)); const clampedY = Math.max(minY, Math.min(maxY, transform.y)); return { ...transform, x: clampedX, y: clampedY, }; }; // ...mutate、groupedTasks、handleDragEnd等逻辑保持不变 return ( <> <h2 className="text-5xl font-black my-10">Tareas</h2> <div ref={taskContainerRef} className="flex gap-5 overflow-x-scroll 2xl:overflow-auto pb-32" > <DndContext onDragEnd={handleDragEnd} modifiers={[restrictToContainer]} > {/* 列渲染逻辑保持不变 */} </DndContext> </div> </> ); }
这样修改后,你的任务卡片就只能在指定的外层容器内拖拽,不会再出现无限滚动的问题,同时完全不影响列与列之间的正常拖拽操作。
内容来源于stack exchange

