動作
アイテムを追加/削除したり、並び替えたり、テキストを編集したりできます。

備忘録がてら、dnd kit の Sortable を利用した実装を残しておきます。
実装
参考: 公式ドキュメント
import {
DndContext,
type DragEndEvent,
DragOverlay,
type DragStartEvent,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type React from "react";
import { useRef, useState } from "react";
// 並び替えるアイテムのprops
interface SortableItemProps {
id: number;
text: string;
}
export const App = () => {
const [activeId, setActiveId] = useState<number | null>(null); // 掴んでいるアイテムのid
const sensors = useSensors(useSensor(PointerSensor)); // センサ
const [items, setItems] = useState<SortableItemProps[]>([]); // アイテムの配列
const refId = useRef(0); // アイテムidの最大値を保持する
// アイテムを追加する
const addItem = () => {
refId.current++;
setItems((items) => [...items, { id: refId.current, text: "" }]);
};
// アイテムを削除する
const removeItem = (id: number) => {
setItems((items) => items.filter((item) => item.id !== id));
};
// ドラッグ開始処理
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
setActiveId(Number(active.id));
};
// ドラッグ終了処理
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active) {
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex); // アイテムを入れ替える
});
}
}
setActiveId(null);
};
// アイテムを更新する
const updateItem = (item: SortableItemProps) => {
setItems((items) => items.map((i) => (i.id === item.id ? item : i))); // idが一致したアイテムを置き換える
};
return (
<div className="flex flex-col items-center m-2">
{/* アイテム追加ボタン */}
<button type="button" onClick={() => addItem()}>+ [Add]</button>
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<SortableContext items={items}>
{items.map((item) => {
return (
<div key={item.id} className="flex" >
{ /* アイテム */ }
<SortableItem item={item} updateItem={updateItem} />
{ /* アイテム削除ボタン */ }
<button type="button" onClick={() => removeItem(item.id)}>x [Remove]</button>
</div>
);
})}
</SortableContext>
{ /* オーバーレイ */ }
<DragOverlay>
{activeId ? (
<SortableItem item={items.find((item) => item.id === activeId)!} updateItem={() => {}} isOverlay={true} />
) : null}
</DragOverlay>
</DndContext>
</div>
);
};
const SortableItem = ({
item,
updateItem,
isOverlay = false, // オーバーレイフラグ
}: {
item: SortableItemProps;
updateItem: (item: SortableItemProps) => void;
isOverlay?: boolean;
}) => {
const {
attributes,
isDragging, // ドラッグ中フラグ
listeners,
setNodeRef,
setActivatorNodeRef, // ドラッグハンドルのアクティベータ
transform,
transition
} = useSortable({
id: item.id,
});
const styleNode: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging || isOverlay ? 2147483647 : undefined, // オーバーレイ中に最前に表示する
visibility: isDragging ? "hidden" : undefined, // オーバーレイ中にもとのノードを表示しない
pointerEvents: isDragging ? "none" : undefined, // オーバーレイ中にもとのノードを表示しない
};
const styleDragHandle: React.CSSProperties = {
cursor: isDragging || isOverlay ? "grabbing" : "grab", // カーソルを変更する
};
// テキストボックスの更新処理
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
item.text = event.target.value; // アイテムのテキストを更新する
updateItem(item); // アイテムを更新する
};
return (
<div ref={setNodeRef} style={styleNode}>
<div className="flex p-2 m-2 border space-x-2">
{ /* ドラッグハンドル */ }
<span ref={setActivatorNodeRef} {...attributes} {...listeners} style={styleDragHandle}>
:::
</span>
{ /* アイテムid */ }
<span>{item.id}</span>
{ /* アイテムtext */ }
<input type="text" value={item.text} onChange={handleTextChange} />
</div>
</div>
);
};
実装時に困ったこと
オーバーレイを追加すると表示が2重になってしまう
単純にオーバーレイを追加しただけでは、ドラッグ元の要素とオーバーレイの要素で表示が重なって表示されてしまします。
今回の実装では、isDraggingフラグを使用して、ドラッグ元の要素を非表示にしています。
カーソルが"grabbing"にならない
アイテムのドラッグ中は、カーソルを"grab"から"grabbing"に変更したく、isDraggingフラグで切り替えていたのですが、オーバーレイ要素のisDraggingがtrueなることはなく、カーソルは変更されませんでした。
今回の実装では、アイテムにオーバーレイフラグを渡して、オーバーレイ要素のカーソルは"grabbing"にするようにしました。