拖放
规则
- 使用
@dnd-kit/core + @dnd-kit/sortable — 现代化、可无障碍访问、支持触摸的拖放库
- 可排序列表:包裹在
<SortableContext> 中,每个项目使用 useSortable() 钩子
- 看板:使用
<DndContext> 和多个 <SortableContext> 容器 — 每列一个容器
- 文件拖放区域:使用
react-dropzone 或原生 onDragOver/onDrop 配合 e.dataTransfer.files
- 触摸支持:dnd-kit 原生处理触摸 — 使用
TouchSensor 和 PointerSensor 并设置激活约束
- 无障碍访问:dnd-kit 提供开箱即用的键盘拖放 —
KeyboardSensor 配合 sortableKeyboardCoordinates
- 乐观重新排序:在拖放时立即更新状态,后台持久化到服务器,出错时回滚
- 拖拽叠加层:使用
<DragOverlay> 实现被拖拽项的浮动预览 — 更好的视觉反馈
模式
import { DndContext, closestCenter, DragOverlay } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
function SortableItem({ id, children }: { id: string; children: React.ReactNode }) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition };
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</div>
);
}
function SortableList({ items }: { items: Item[] }) {
const [activeId, setActiveId] = useState<string | null>(null);
return (
<DndContext
collisionDetection={closestCenter}
onDragStart={({ active }) => setActiveId(active.id)}
onDragEnd={handleDragEnd}
>
<SortableContext items={items.map((i) => i.id)} strategy={verticalListSortingStrategy}>
{items.map((item) => (
<SortableItem key={item.id} id={item.id}>{item.name}</SortableItem>
))}
</SortableContext>
<DragOverlay>{activeId ? <ItemPreview id={activeId} /> : null}</DragOverlay>
</DndContext>
);
}
避免
- 直接使用 HTML5 拖放 API — 它在触摸/移动/无障碍方面表现不佳
- 忘记键盘无障碍访问 — 始终包含
KeyboardSensor
- 在拖拽过程中更新状态 — 仅在
onDragEnd 时更新
- 遗漏
DragOverlay — 没有它,原始项目在拖拽时会消失且无视觉反馈