0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

dnd kit で要素の並び替えを簡単実装してみた

0
Posted at

動作

アイテムを追加/削除したり、並び替えたり、テキストを編集したりできます。
画面収録 2025-08-16 18.46.53-soConvert.gif
備忘録がてら、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"にするようにしました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?