dnd kit でソート可能アイテム内にボタンがある時にボタンのクリックとアイテムのソートを両方できるようにする
こんにちは、 Gaji-Labo アシスタントエンジニアの石垣です。
今回は、ドラッグ&ドロップでソートするコンポーネントを実装できる React 用ライブラリの dnd kit で、ソート可能アイテム内にボタンがある時にボタンのクリックとアイテムのソートを両方できるようにする方法についてまとめてみます。
前回の記事の応用的な部分もありますので、そちらも併せてお読みいただければと思います。
やりたいこと
前回の記事では、ソート可能アイテムの中にボタンがある時、以下のようにドラッグイベント or クリックイベントのどちらかを発火するようにイベントの発火領域を切り分ける方法を取っていました。
これでも解決は出来るのですが、時には以下のようにソート可能アイテムのほぼ全体にボタンを置きたいケースがあるかと思います。
こういったケースの場合は dnd kit に用意されている、マウスやキーボードなどのデバイスからのイベントを検知できるユーティリティの useSensor を使うことで、ドラッグイベントの発生を遅らせて、クリックした時はクリックイベントを、ドラッグした時はドラッグイベントをそれぞれ発火させることが可能になります。
実装
ソート可能アイテムの実装
import { useCallback, useState } from "react";
import {
DndContext,
KeyboardSensor,
MouseSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Box, Button, Modal } from "@mui/material";
const items = [
"http://placekitten.com/g/200/300",
"http://placekitten.com/g/300/300",
"http://placekitten.com/g/400/300",
"http://placekitten.com/g/500/300",
];
const contents = items.map((item) => ({
id: item,
content: item,
}));
interface SortableItemProps {
id: string;
imgSrc: string;
index: number;
}
function SortableItem({ id, imgSrc, index }: SortableItemProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 1 : 0,
// スタイル調整用
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: 4,
borderRadius: 4,
width: "150px",
height: "150px",
border: "1px solid black",
backgroundColor: "white",
padding: 16,
};
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Button onClick={handleOpen} sx={{ width: "100%", height: "100%" }}>
<img
src={imgSrc}
alt={`${index + 1}番目の画像のサムネイル`}
style={{
height: "100%",
width: "100%",
objectFit: "contain",
}}
/>
</Button>
<Modal open={open} onClose={handleClose}>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
border: "2px solid #000",
boxShadow: 24,
p: 4,
}}
>
<img
src={imgSrc}
alt={`${index + 1}番目の画像`}
style={{
height: "100%",
width: "100%",
objectFit: "contain",
}}
/>
</Box>
</Modal>
</div>
);
}
ソート可能アイテムの実装はほぼ前回作成したコンポーネントの流用です。
SortableItem
内で、MUI の Modal コンポーネントを使用し、サムネイルをクリックした際にモーダルで画像を表示する実装を追加しています。
ソート可能領域の実装
function DndSample(): JSX.Element {
const [state, setState] =
useState<{ id: string; content: string }[]>(contents);
const handleDragEnd = useCallback(
(event) => {
const { active, over } = event;
if (over === null) {
return;
}
if (active.id !== over.id) {
const oldIndex = state
.map((item) => {
return item.id;
})
.indexOf(active.id);
const newIndex = state
.map((item) => {
return item.id;
})
.indexOf(over.id);
const newState = arrayMove(state, oldIndex, newIndex);
setState(newState);
}
},
[state]
);
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 5, // 5px ドラッグした時にソート機能を有効にする
},
});
const keyboardSensor = useSensor(KeyboardSensor);
const sensors = useSensors(mouseSensor, keyboardSensor);
return (
<DndContext onDragEnd={handleDragEnd} sensors={sensors}>{/* DndContext に sensors を渡して有効化 */}
<SortableContext items={state}>
{state.map((item, index) => (
<SortableItem
key={item.id}
id={item.id}
imgSrc={item.content}
index={index}
/>
))}
</SortableContext>
</DndContext>
);
}
ポイントとしては、mouseSensor
に activationConstraint: {distance: 5}
を渡しています。
これは 5px ドラッグしたらソート機能を有効にする
という実装です。
これでクリックした時 = ドラッグしていない時にはソートせずにボタンのクリックイベントを発火させることが可能になります。
まとめ
今回はドラッグ&ドロップでソートするコンポーネントを実装できる React 用ライブラリの dnd kit で、ソート可能アイテム内にボタンがある時にボタンのクリックとアイテムのソートを両方できるようにする方法についてまとめてみました。
dnd kit を使っている方の参考にしていただければと思います。
Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています
弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!
求人応募してみる!