dnd kitを使ったかんばんボードの並び替えを実装する
はじめに
こんにちは、dotDの浅野です。
この記事では、React のドラッグ&ドロップライブラリである dnd kit を使用して、かんばんボードのタスクを並び替える方法を解説します。
完成イメージ
実装するかんばんボードの完成イメージです。
使用技術
- Next.js
- dnd kit
- TypeScript
dnd kit とは?
dnd kit は、シンプルで柔軟性の高いドラッグ&ドロップライブラリ です。
リストの並び替え、ボードの構築、カスタム UI など、多様なユースケースに対応可能で、パフォーマンスやアクセシビリティも考慮した設計 になっています。
環境構築
1. Next.jsのインストール
yarn create next-app --typescript .
2. 必要なライブラリのインストール
yarn add @dnd-kit/core @dnd-kit/sortable
dnd kit を使ったドラッグ&ドロップの実装
コード全体は以下をご確認ください。
ボード全体の実装
1. dnd kitのセットアップ
dnd Kit を使用するには、DndContext
をラップし、ドラッグ&ドロップを制御します。
本実装では、以下の2つのイベントをハンドリングします。
-
onDragEnd
: 同じセグメント内の並び替え -
onDragOver
: 異なるセグメントへの移動
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
>
<div className={styleContainerWrapper}>
{data.map((group, index) =>
<Segment key={index} segmentItem={group} />
)}
</div>
</DndContext>
-
collisionDetection={closestCorners}
でドラッグ判定を最適化 -
Segment
コンポーネント でセグメントごとにタスクを描画
また、ユーザーの操作を検知する センサー (sensors) も設定します。
const sensors = useSensors(
useSensor(PointerSensor), // マウス・タッチ操作を検知
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) // キーボード操作をサポート
);
2. データ構造
タスクは data
ステートで管理し、各セグメントごとにタスクを保持します。
const [data, setData] = useState<ISegmentItem[]>([
{ id: "segment1", title: "未進行", tasks: [{ id: "item1", title: "Task 1" }, { id: "item4", title: "Task 4" }] },
{ id: "segment2", title: "進行中", tasks: [{ id: "item2", title: "Task 2" }, { id: "item5", title: "Task 5" }] },
{ id: "segment3", title: "完了", tasks: [{ id: "item3", title: "Task 3" }, { id: "item6", title: "Task 6" }] },
]);
ドラッグ時のアイテム判別に使用するためタスクの id
は セグメントと重複しないようにする 必要があります。
3. ドラッグされたアイテムの所属を特定
タスクがどのセグメントに属しているかを特定するため、findSegment
関数を定義します。
const findSegment = (id: string | null) => {
if (!id) return null;
return data.find((segment) =>
segment.id === id || segment.tasks.some((task) => task.id === id)
) ?? null;
};
- セグメント ID の場合 → そのまま返す
- タスク ID の場合 → 所属するセグメントを返す
4. 同じセグメント内での並び替え
同じセグメント内の並び替えは、onDragEnd
で処理します。
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
const activeSegment = findSegment(String(active.id));
const overSegment = findSegment(over ? String(over.id) : null);
if (!activeSegment || !overSegment || activeSegment !== overSegment) return null;
setData((prevState) => prevState.map((segment) => {
if (segment.id === activeSegment.id) {
segment.tasks = arrayMove(segment.tasks,
activeSegment.tasks.findIndex((task) => task.id === active.id),
activeSegment.tasks.findIndex((task) => task.id === over?.id)
);
}
return segment;
}));
};
-
arrayMove
を使って、同じセグメント内での並び替えを実装 - 異なるセグメントなら処理しない
5. 異なるセグメント間の移動
異なるセグメント間でタスクを移動させる場合、onDragOver
で処理します。
const handleDragOver = (event: DragOverEvent) => {
const { active, over, delta } = event;
const activeSegment = findSegment(String(active.id));
const overSegment = findSegment(over ? String(over.id) : null);
if (!activeSegment || !overSegment || activeSegment === overSegment) return null;
setData((prevState) => {
const activeItems = activeSegment.tasks;
const overItems = overSegment.tasks;
const activeIndex = activeItems.findIndex((task) => task.id === active.id);
const overIndex = overItems.findIndex((task) => task.id === over?.id);
const newIndex = () => {
const putOnLastItem = overIndex === overItems.length - 1 && delta.y > 0;
return overIndex >= 0 ? overIndex + (putOnLastItem ? 1 : 0) : overItems.length + 1;
};
return prevState.map((s) => {
if (s.id === activeSegment.id) {
s.tasks = activeItems.filter((t) => t.id !== active.id);
} else if (s.id === overSegment.id) {
s.tasks = [
...overItems.slice(0, newIndex()),
activeItems[activeIndex],
...overItems.slice(newIndex())
];
}
return s;
});
});
};
- 異なるセグメントにドロップされたら移動
-
delta.y > 0
を使い、最後のカードの下にドロップした場合の処理を考慮
セグメント(列)のコンポーネント実装
Segment
コンポーネントは、かんばんボードの各列 を表し、Dnd Kit
を使用してタスクのドラッグ&ドロップを可能にする実装です。
各セグメント(列)は タスクのリスト を持ち、ドラッグ&ドロップ時の並び替えをサポートします。
useDroppable
によるドロップ領域の設定
1. const { setNodeRef } = useDroppable({ id: segmentItem.id });
-
setNodeRef
をdiv
に渡すことで、この列がドラッグ&ドロップのターゲット であることを dnd kit に伝えます。
SortableContext
による並び替え制御
2. <SortableContext id={segmentItem.id} items={segmentItem.tasks} strategy={rectSortingStrategy}>
-
SortableContext
を使い、列内のタスクが並び替え可能であることを定義 します。 -
rectSortingStrategy
は タスクを縦に並び替えるための指定 です。
タスク(アイテム)のコンポーネント実装
Task
コンポーネントは、かんばんボードの各タスク を表し、dnd kit を使用して ドラッグ&ドロップで並び替え可能 にする実装です。
useSortable
によるドラッグ可能な設定
1. const { attributes, listeners, setNodeRef, transform } = useSortable({ id: taskItem.id });
-
useSortable
にtaskItem.id
を渡し、タスクごとにドラッグ&ドロップ可能な要素 として設定します。 - 各プロパティの役割:
-
setNodeRef
:要素を dnd kit に関連付ける -
attributes
:ドラッグ可能な要素としての属性を追加 -
listeners
:ドラッグ操作を開始するためのリスナー -
transform
:ドラッグ中の 位置情報 を取得
-
transform
を適用し、ドラッグ時の動きをスムーズにする
2. const style = {
transform: CSS.Transform.toString(transform),
};
-
transform
をCSS.Transform.toString
で変換し、ドラッグ時にスムーズに移動 させます。
3. タスクの描画
<div ref={setNodeRef} style={style} {...attributes} {...listeners} className={styleItem}>
<h3>{taskItem.title}</h3>
</div>
-
setNodeRef
をdiv
に設定し、ドラッグ可能な要素として認識 させます。 -
attributes
とlisteners
を適用し、ドラッグ操作が可能に なります。
まとめ
- dnd kit を使用してかんばんボードの並び替えを実装する方法を解説しました。
- より高度なカスタマイズ(カラムの並び替えなど)も可能です。詳しくは公式ドキュメントをご確認ください。
弊社では事業創造にチャレンジする仲間を募集中です。
ぜひ採用サイトをご覧いただければ幸いです。
Discussion