在拖放项目时,占位符会立即消失,而不是等待覆盖层的过渡动画
我在一个 Next.js 项目中使用 dnd-kit 来在侧边栏重新排序项目。拖动工作正常,但在释放时存在一个视觉问题。
在拖动过程中,我在列表中渲染一个 占位符元素(isPlaceholder),同时通过一个 DragOverlay 显示被拖拽的项。
释放时会出现问题:
- 一旦我松手,占位符会立即消失。
- 同时,
DragOverlay会回到列表中的位置并进行动画。 - 由于占位符被立即移除,界面出现了明显的硬切换。
我更希望的是:
- 占位符应该在Overlay完成其过渡之前保持可见。
- 换句话说,覆盖层应在占位符消失之前,视觉上落在其上方。
当前行为:
- 拖动项 → 占位符出现在列表中
- 松手 → 占位符会立即消失
- 覆盖层回到位置的动画 → 视觉跳跃
期望行为:
- 拖动项 → 占位符出现
- 松手 → 覆盖层向后回到原位并完成动画
- 占位符在Overlay完成过渡后才消失
下面是渲染每个可排序项的组件。拖动把手通过 dragHandleProps 传递给子组件。
"use client";
import React, { useState } from "react";
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from "@dnd-kit/core";
import {
SortableContext,
useSortable,
arrayMove,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
type Shelf = {
id: number;
name: string;
};
const initialShelves: Shelf[] = [
{ id: 1, name: "Shelf A" },
{ id: 2, name: "Shelf B" },
{ id: 3, name: "Shelf C" },
];
function SortableItem({
shelf,
selectedShelfs,
setSelectedShelfs,
selectedShelfId,
setSelectedShelfId,
isPlaceholder,
}: any) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: shelf.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li ref={setNodeRef} style={style}>
{isPlaceholder ? (
<div className="p-3 mb-2 rounded-md bg-red-600 border border-red-700 h-[44px]" />
) : (
<div
className={`
flex items-center gap-2 p-3 mb-2 rounded-md border text-black bg-white
${selectedShelfId === shelf.id ? "border-blue-500 border-2" : "border-gray-300"}
`}
>
<span
{...attributes}
{...listeners}
className="cursor-grab select-none px-2 py-1 rounded bg-gray-200"
>
☰
</span>
<input
type="checkbox"
checked={selectedShelfs.includes(shelf.id)}
onChange={() =>
setSelectedShelfs((prev: number[]) =>
prev.includes(shelf.id)
? prev.filter((id) => id !== shelf.id)
: [...prev, shelf.id],
)
}
/>
<div
className="flex-1 cursor-pointer"
onClick={() => setSelectedShelfId(shelf.id)}
>
{shelf.name}
</div>
</div>
)}
</li>
);
}
function OverlayItem({ shelf }: { shelf: Shelf }) {
return (
<div className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80">
<span className="px-2 py-1 rounded bg-gray-200">☰</span>
<span>{shelf.name}</span>
</div>
);
}
export default function Home() {
const [shelves, setShelves] = useState(initialShelves);
const [selectedShelfs, setSelectedShelfs] = useState<number[]>([]);
const [selectedShelfId, setSelectedShelfId] = useState<number | null>(null);
const [activeId, setActiveId] = useState<number | null>(null);
const sensors = useSensors(useSensor(PointerSensor));
function handleDragEnd(event: any) {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = shelves.findIndex((s) => s.id === active.id);
const newIndex = shelves.findIndex((s) => s.id === over.id);
setShelves((items) => arrayMove(items, oldIndex, newIndex));
}
setActiveId(null);
}
const activeShelf = shelves.find((s) => s.id === activeId);
return (
<div className="max-w-md mx-auto mt-10 font-sans">
<h3 className="mb-4 text-lg font-semibold">Shelves</h3>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={(e) => setActiveId(e.active.id as number)}
onDragEnd={handleDragEnd}
onDragCancel={() => setActiveId(null)}
>
<SortableContext
items={shelves.map((s) => s.id)}
strategy={verticalListSortingStrategy}
>
<ul className="list-none p-0">
{shelves.map((shelf) => (
<SortableItem
key={shelf.id}
shelf={shelf}
selectedShelfs={selectedShelfs}
setSelectedShelfs={setSelectedShelfs}
selectedShelfId={selectedShelfId}
setSelectedShelfId={setSelectedShelfId}
isPlaceholder={activeId === shelf.id}
/>
))}
</ul>
</SortableContext>
<DragOverlay>
{activeShelf ? <OverlayItem shelf={activeShelf} /> : null}
</DragOverlay>
</DndContext>
</div>
);
}
示例:
解决方案
视觉跳跃之所以发生,是因为 DndContext 在松手时会立即触发 onDragEnd,在 DragOverlay 完成过渡之前就清除了你的 activeId。
为修复此问题,请将 setActiveId(null) 移到 DragOverlay's onDragEnd 属性上,这会等待动画完成后才触发。
function handleDragEnd(event) {
const { active, over } = event;
if (over && active.id !== over.id) {
setShelves((items) => arrayMove(items, oldIndex, newIndex));
}
// Remove setActiveId(null) from here
}
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={items}>
{items.map(item => (
<SortableItem isPlaceholder={activeId === item.id} />
))}
</SortableContext>
<DragOverlay onDragEnd={() => setActiveId(null)}>
{activeItem ? <Item /> : null}
</DragOverlay>
</DndContext>
DndContext 会立即重新排序列表,将占位符移动到目标位置。DragOverlay 向那个占位符进行动画靠拢。DragOverlay.onDragEnd 只有在真实项落地后才将占位符替换为实际项。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。