拖拽排序
带键盘对等、放置指示、乐观持久化的指针重排序。
组件模式高级
为什么
拖拽是按空间直觉重排项的最快方式。但仅指针 DnD 排除键盘和触摸用户且易出错(卡顿、无放置提示)。指针+键盘+触摸方案服务所有人。
何时使用
用于列表内重排、看板列或树节点。避免 >50 项的跨容器移动 — 认知负荷胜过移至下拉。
如何做
1. 选库 — @dnd-kit/core(无障碍,推荐)或 react-beautiful-dnd(旧)
2. 指针 — onPointerDown 捕获项,拖拽覆盖跟随光标
3. 放置指示 — 2px 线显示项将落处,由指针在目标上计算
4. 键盘 — Space 拾起、箭头键移动、Space 放下;通过 aria-live 播报
5. 触摸 — 长按启动(避免与滚动冲突),或专用拖拽手柄
6. 乐观 — 立即重排本地状态,后台 PATCH 服务器,出错回滚
7. 约束 — 列表锁定一轴、限制放置区、看板吸附栅格
示例
import { DndContext, useDraggable, useDroppable, PointerSensor, KeyboardSensor } from "@dnd-kit/core";
function SortableItem({ id, label }: { id: string; label: string }) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({ id });
const style = transform ? { transform: `translate(${transform.x}px,${transform.y}px)` } : undefined;
return (
<li ref={setNodeRef} style={style} {...listeners} {...attributes}
className="cursor-grab focus:ring-2" tabIndex={0}>
{label} <span className="sr-only">按 Space 拖拽,箭头移动</span>
</li>
);
}
function SortableList({ items, onReorder }: Props) {
return (
<DndContext sensors={[useSensor(PointerSensor), useSensor(KeyboardSensor)]}
onDragEnd={(e) => onReorder(e.active.id, e.over.id)}>
<ul role="list">
{items.map((it) => <SortableItem key={it.id} id={it.id} label={it.label} />)}
</ul>
</DndContext>
);
}
常见问题
- ⚠仅指针 — 键盘用户无法重排,未过 WCAG 2.1.1 键盘可操作
- ⚠无放置指示 — 用户盲放,项落在意外处
- ⚠拖拽与滚动冲突 — 触摸用户无法滚动列表,每个手势启动拖拽
最佳实践
- ✓始终含 KeyboardSensor — Space 拾起、箭头移动、Space 放下
- ✓显示放置指示线 — 用户释放前需看到项将落处
- ✓触摸用拖拽手柄 — 长按手柄拖拽,其余可自由滚动