🀄

Reactのドラッグ&ドロップライブラリ、dnd kitの使い方をユースケースから理解する

2023/11/22に公開

こんにちは!株式会社 CastingONEの岡本です。

はじめに

弊社のアプリケーションのフロントは現在、Nuxt2 から React(Next.js)にリプレイスを行なっています。移行するにたり、ドラッグ&ドロップのライブラリを探していたところdnd kitが良さそうということになったので、このライブラリを深く理解するため、社内で勉強会を開催を実施しました。今回は、その勉強会で使用したサンプルを基に、dnd kit の使い方について解説していきます!

dnd kit とは

https://docs.dndkit.com/
dnd kit は、React のための軽量かつ拡張可能なドラッグ&ドロップのツールキットです。主な特徴は以下の通りです。

  • 豊富な機能
    カスタマイズ可能な衝突検出アルゴリズム、複数のアクティベータ、ドラッグ可能なオーバーレイ、ドラッグハンドル、自動スクロール、制約などが含まれる
  • React 専用
    useDraggable や useDroppable などのフックを公開しており、アプリの再設計や追加のラッパー DOM ノードの作成を必要としない。
  • 多様な使用ケース
    リスト、グリッド、複数のコンテナ、ネストされたコンテキスト、可変サイズのアイテム、仮想リスト、2D ゲームなどに対応している
  • ゼロ依存性とモジュラー
    ライブラリのコアは約 10kb(圧縮済み)で、外部依存性はありません。React の組み込みの状態管理とコンテキストを中心に構築されています。
  • 複数の入力方法に対応
    ポインタ、マウス、タッチ、キーボードセンサーをサポート
  • カスタマイズ可能&拡張機能
    アニメーション、遷移、動作、スタイルのすべての詳細をカスタマイズ可能。独自のセンサーや衝突検出アルゴリズムを構築できます。
  • アクセシビリティ
    キーボードサポート、デフォルトの aria 属性、カスタマイズ可能なスクリーンリーダーの指示、組み込みのライブリージョンを提供
  • プリセット
    dnd kit には、アイテムの並べ替え機能を簡単に実装できるSortableプリセットが用意されています。

基本的な使い方

それでは、dnd kit の簡単な使い方を以下の codesandbox のブロックをドロップエリアにドラッグしたらカウントアップされる実装を見ならがら解説していきます。

dnd kit でドラッグ&ドロップをするために@dnd-kit/coreをインストールします。

yarn add @dnd-kit/core

そして@dnd-kit/coreDndContext, useDroppable, useDraggableを使用します。

  • DndContext
    dnd kit の中心的なコンポーネントで、ドラッグ&ドロップの動作のコンテキストを提供します。このコンポーネントの中にドラッグ可能およびドロップ可能な要素を配置することで、ドラッグ&ドロップのインタラクションが有効になります。
    https://docs.dndkit.com/api-documentation/context-provider

    DndContext の Props、イベントオブジェクト

    Props

    • announcements
      スクリーンリーダーのアナウンスメントをカスタマイズするためのもの。ドラッグをスタートしたタイミングなどで、スクリーンリーダーで読み上げられる文章を変更できます。
    • screenReaderInstructions
      スクリーンリーダーの指示をカスタマイズするためのもの。
    • sensors
      ドラッグ操作を開始、移動、終了またはキャンセルするための異なる入力方法を検出する時に使用
    • accessibility
      ARIA 属性などを設定できます
    • autoScroll
      すべてのセンサーの自動スクロールを一時的または恒久的に無効にするために使用
    • cancelDrop
      ドロップ操作をキャンセルするための関数を提供
    • collisionDetection
      衝突検出アルゴリズムをカスタマイズするための props
    • modifiers
      センサーによって検出される移動座標を動的に変更するための props
    • onDragStart
      ドラッグが開始されたときに発火するイベントハンドラ
    • onDragMove
      ドラッグアイテムが移動されるたびに発火するイベントハンドラ
    • onDragOver
      ドラッグアイテムがドロップ可能なコンテナの上に移動されたときに発火するイベントハンドラ
    • onDragEnd
      ドラッグアイテムがドロップされた後に発火するイベントハンドラ
    • onDragCancel
      ドラッグ操作がキャンセルされた場合に発火するイベントハンドラ

    イベントオブジェクト

    onDragStartonDragEndのようなイベントハンドラーで取得できる event の中身は以下の通りです。

    active

    activeは現在ドラッグ中の要素に関する情報を持っています。

    • id
      useDraggableで指定された ID
    • data
      useDraggableで指定されたデータが格納される
    • disabled
      要素が使用不可かどうかを示す bool 値
    • node
      要素のノード情報
    • rect
      要素の位置やサイズ情報

    over

    overは、ドラッグアイテムが交差しているドロップ先の情報を示します。

    • id
      useDroppableで指定された ID
    • data
      useDroppableで指定されたデータが格納される
    • disabled
      ドロップ先がドロップ可能かどうかを示す bool 値
    • rect
      要素の位置やサイズ情報
  • useDroppable
    この hook を使用すると、特定のコンポーネントをドロップ可能なターゲットとして設定することができます。例えば、ドラッグされたアイテムを受け入れる場所や、特定のタイプのアイテムのみを受け入れるような設定が可能です。
    https://docs.dndkit.com/api-documentation/droppable/usedroppable

    useDroppable の Args, Props

    Arguments

    useDroppableの引数の I/F は以下の通りです。

    interface UseDroppableArguments {
      id: string | number;
      disabled?: boolean;
      data?: Record<string, any>;
    }
    
    • id
      ドロップ可能な要素の一意の識別子です。同じ DndContext プロバイダ内で他のドロップ可能な要素と同じ識別子を共有することはできません。
    • disables
      ドロップ可能なエリアを一時的に無効にするための bool 値です。
    • data
      droppable 要素に持たせたいdataを指定することができます。例えば、DndContextonDragStartのようなイベントハンドラーで取得できるeventoverdataに格納することができます。

    Properties

    useDroppableの返り値の I/F は以下の通りです。

    {
      setNodeRef(element: HTMLElement | null): void;
      isOver: boolean;
      rect: React.MutableRefObject<LayoutRect | null>;
      node: React.RefObject<HTMLElement>;
      over: {id: UniqueIdentifier} | null;
    }
    
    • setNodeRef
      ドロップ可能なエリアとして機能する HTML 要素に関連付けるためのものです。setNodeRef は、ドロップ可能なエリアの DOM ノードへの参照を保持します。これにより、dnd kit はそのエリアの位置やサイズを知ることができ、ドラッグ&ドロップの動作を正確に制御することができます。
    • isOver
      ドラッグ中の要素がドロップ可能なエリアの上にあるかどうかを示します。true の場合、ドラッグ中の要素が現在のドロップ可能なエリアの上にあります。このプロパティを使用して、ドラッグ中の要素がドロップ可能なエリアの上にあるときのエリアのスタイルや動作をカスタマイズすることができます。
    • rect
      ドロップ可能なエリアの境界矩形の測定を保持します。
    • node
      こドロップ可能なエリアとして機能する HTML 要素への参照を保持します。
    • over
      現在ドロップ可能なエリアの上にあるドラッグの一意の識別子を示します。ドラッグアイテムがドロップ可能なエリアの上にない場合、この値は null になります
  • useDraggable
    この hook を使用すると、コンポーネントをドラッグ可能にすることができます。ドラッグ時の見た目や、ドラッグアイテムのタイプを指定したりすることができます。
    https://docs.dndkit.com/api-documentation/draggable/usedraggable

    useDraggable の Args, Props

    Arguments

    useDraggableの引数の I/F は以下の通りです。

    interface UseDraggableArguments {
      id: string | number;
      attributes?: {
        role?: string;
        roleDescription?: string;
        tabIndex?: number;
      };
      data?: Record<string, any>;
      disabled?: boolean;
    }
    
    • id
      ドラッグ可能な要素の一意の識別子です。同じ DndContext プロバイダ内で他のドラッグ可能な要素と同じ識別子を共有することはできません。
    • attributes
      ARIA 属性や tabIndex を指定するためのオブジェクトです。これにより、アクセシビリティを向上させることができます。
    • data
      ドラッグ可能な要素に関連する任意のデータを格納するためのオブジェクトです。
    • disabled
      ドラッグ可能な要素を一時的に無効にするための bool 値です。

    Properties

    useDraggableの返り値の I/F は以下の通りです。

    {
      active: {
        id: UniqueIdentifier;
        node: React.MutableRefObject<HTMLElement>;
        rect: ViewRect;
      } | null;
      attributes: {
        role: string;
        tabIndex: number;
        ‘aria-diabled’: boolean;
        ‘aria-roledescription’: string;
        ‘aria-describedby’: string;
      },
      listeners: Record<SyntheticListenerName, Function> | undefined;
      isDragging: boolean;
      node: React.MutableRefObject<HTMLElement | null>;
      over: {id: UniqueIdentifier} | null;
      setNodeRef(HTMLElement | null): void;
      setActivatorNodeRef(HTMLElement | null): void;
      transform: {x: number, y: number, scaleX: number, scaleY: number} | null;
    }
    
    • active
      現在ドラッグ中の要素に関する情報。id は要素の識別子、node は要素への参照、rect は要素の位置やサイズ情報を示す。
    • attributes
      ARIA 属性や tabIndex を指定するためのオブジェクト。これにより、アクセシビリティを向上させることができます。
    • listeners
      イベントリスナーの集合。ドラッグ&ドロップの動作を制御するために内部的に使用されます。
    • isDragging
      要素が現在ドラッグ中かどうかを示す bool 値。
    • node
      ドラッグ可能な要素として機能する HTML 要素への参照。
    • over
      現在ドラッグ中の要素がドロップ可能なエリアの上にある場合、そのエリアの識別子を持つオブジェクト。そうでない場合は null。
    • setNodeRef
      ドラッグ可能な要素として機能する HTML 要素に関連付けるための関数。
    • setActivatorNodeRef
      ドラッグの開始をトリガーする要素への参照を設定するための関数。
    • transform
      ドラッグ中の要素の移動やスケーリングに関する情報を持つオブジェクト。ドラッグアイテムがドロップ可能なエリアの上にない場合、この値は null になります。

useDroppable

まずは、useDroppable を使用したコンポーネントを見ていきましょう。

Droppable.tsx
import { useDroppable } from "@dnd-kit/core";
import { Box, Stack, Typography } from "@mui/material";
import { FC, ReactNode } from "react";

type DroppableProp = {
  children: ReactNode;
  id: string
};

export const Droppable: FC<DroppableProp> = ({ children, id }) => {
  const {
    setNodeRef,
    isOver
  } = useDroppable({
    id
  })

  return (
    <Box
      ref={setNodeRef}
      sx={{
        width: "200px",
        minHeight: "300px",
        bgcolor: isOver ? "lightGreen" : undefined,
        overflowX: "auto",
        padding: 2,
        border: "1px solid black"
      }}
    >
      <Stack spacing={2}>
        <Typography sx={{ fontWeight: "bold" }}>ドロップエリア</Typography>
        {children}
      </Stack>
    </Box>
  )
}

useDroppableを呼び出し、引数にドロップ可能なエリアをユニークに識別するためのidを渡します。そして、返り値でsetNodeRefisOverを受け取ります。setNodeRefはドロップ可能なエリアの DOM ノードを参照するために使用されます。これを操作したい要素に渡すことで、その要素がドロップ可能なエリアとして機能するようになります。isOverはドラッグ中のアイテムがドロップ領域と重なっているかどうかを示す boolean です。isOverが true の時にスタイルを変更することにより、「ここにアイテムを置けるよ!!」というヒントを見せることができます。

useDraggable

次に、useDraggable の実装を見ていきます。

DraggableBlockSource.tsx
import { FC } from "react";

type DraggableBlockSourceType = {
  isDragging?: boolean;
  label: string;
};

export const DraggableBlockSource: FC<DraggableBlockSourceType> = ({
  isDragging,
  label
}) => {
  return (
    <div
      style={{
        textAlign: "center",
        padding: 20,
        border: "solid 1px black",
        backgroundColor: "#fff",
        userSelect: "none",
        cursor: isDragging ? "grabbing" : "grab",
        opacity: isDragging ? 0.5 : undefined,
        width: "fit-content"
      }}
    >
      {label}
    </div>
  );
};
Draggable.tsx
import { useDraggable } from "@dnd-kit/core";
import { FC } from "react";
import { DraggableBlockSource } from "./DraggableBlockSource";

type Props = {
  id: string;
  label: string;
};

export const Draggable: FC<Props> = ({ id, label }) => {
  // useDraggableを使って必要な値をもらう
  const {
    setNodeRef,
    listeners,
    attributes,
    transform,
    isDragging
  } = useDraggable({
    id
  });

  const transformStyle = transform
    ? `translate(${transform.x}px, ${transform.y}px)`
    : undefined;

  return (
    <div
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      style={{
        transform: transformStyle,
        height: "fit-content"
      }}
    >
      <DraggableBlockSource isDragging={isDragging} label={label} />
    </div>
  );
};

useDraggable を呼び出し、useDraggable と同様に idを渡します。そして、返り値で setNodeReflistenersattributestransform を受け取ります。setNodeRef はドラッグアイテムの DOM ノードへの参照を設定するための関数です。listeners はドラッグ操作を開始、進行、終了するためのイベントリスナーを含むオブジェクトです。これをドラッグアイテムに渡すことで、ドラッグ操作を有効にできます。attributes はドラッグ可能な要素に適用する必要がある属性を含むオブジェクトです。transform はドラッグ中の要素の位置を表すオブジェクトです。xyの 2 つのプロパティを持ち、それぞれ要素の水平および垂直の移動距離を示します。transform を使うことにより、ドラッグ中の要素のスタイルを動的に更新しています。

DndContext

最後に DndContext を呼び出しているApp.tsxを見ていきます。

App.tsx
import { DndContext } from "@dnd-kit/core";
import { Box, Stack } from "@mui/material";
import { useState } from "react";
import { Draggable } from "./components/Draggable/Draggable";
import { Droppable } from "./components/Droppable/Droppable";

export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
          onDragEnd={(event) => {
            const { over } = event;
            if (over == null) {
              return;
            }
            setDropCount((x) => x + 1);
          }}
        >
          <Box
            sx={{
              mb: 5
            }}
          >
            <Draggable id="draggableA" label="ドラッグブロック" />
          </Box>
          <Droppable id="dropAreaA">{dropCount}回ドロップしたぜ</Droppable>
        </DndContext>
      </Box>
    </div>
  );
}

DndContextの中に上述の<Draggable><Droppable>を使用します。DndContextonDragEndのようなイベントハンドラーが提供されます。onDragEndはドラッグ操作が終了(ドロップされた時)に呼び出されます。他のイベントハンドラーはonDragStartonDragOverなどがあります。イベントハンドラー内でeventオブジェクトを受け取ることができ、その中のoverプロパティを取得しています。overプロパティはドラッグが終了した時に乗っかっているドロップ領域を示すオブジェクトです。overが存在する = ドロップ領域にアイテムが乗っかっているということになるので、その場合はインクリメントするようにしています。

これが、dnd kit の最小構成での使用方法です。次章からはユースケースに従って、機能の肉付けをしていき、dnd kit のさまざまな機能と設定方法を詳しく見ていきます。

ドラッグアイテムが複数ある場合

以下の codesandbox のように、ドラッグアイテムが複数あり、アイテムによってドロップカウントされる対象が違うものを実装します。

ドラッグアイテムに独自の情報を持たせたい場合は、useDraggableの引数のdataに格納します。今回はlabelを持たせて、イベントハンドラー側で操作します。

Draggable.tsx
...省略

type Props = {
  id: string;
  label: string;
};

export const Draggable: FC<Props> = ({ id, label }) => {
  const {
    setNodeRef,
    listeners,
    attributes,
    transform,
    isDragging
  } = useDraggable({
    id,
+   data: {
+     label
+   }
  });

  ....省略
};

上述の対応で、ドラッグアイテムがlabelの情報を持っているので、それをイベントで拾って加算している部分を修正していきます。

App.tsx
...省略

export default function App() {
  // ドロップカウント
  const [dropCountApple, setDropCountApple] = useState(0);
  const [dropCountBanana, setDropCountBanana] = useState(0);
  const [dropCountOrange, setDropCountOrange] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
          onDragEnd={(event) => {
-           const { over } = event;
-           if (over == null) {
+           const { over, active } = event;
+           if (over == null || active.data.current == null) {
              return;
            }
-           setDropCount((x) => x + 1);
+           switch (active.data.current.label) {
+             case "りんご":
+               setDropCountApple((x) => x + 1);
+               break;
+             case "ばなな":
+               setDropCountBanana((x) => x + 1);
+               break;
+             case "みかん":
+               setDropCountOrange((x) => x + 1);
+               break;
+           }
          }}
        >
          <Stack
            direction="row"
            spacing={2}
            sx={{
              mb: 5
            }}
          >
-           <Draggable id="draggable" label="ドラッグアイテム"
+           <Draggable id="draggable1" label="りんご" />
+           <Draggable id="draggable2" label="ばなな" />
+           <Draggable id="draggable3" label="みかん" />
          </Stack>
          <Droppable id="dropAreaA">
+           <div>りんごを{dropCountApple}回ドロップしたぜ</div>
+           <div>ばななを{dropCountBanana}回ドロップしたぜ</div>
+           <div>みかんを{dropCountOrange}回ドロップしたぜ</div>
          </Droppable>
        </DndContext>
      </Box>
    </div>
  );
}

イベントハンドラーで受け取れるeventの中に、現在ドラッグしている要素のactiveというオブジェクトがあるので、そちらを取得します。Draggabledataに入れた情報はdata.currentの中に格納されているので、そこからlabelを switch 文で対応するドロップカウントを加算するようにしています。

このようにdataを使用することで、ドラッグ中の要素が持つ特定の情報に基づいて、ドロップ時の振る舞いや処理を柔軟にカスタマイズすることが可能になります!

ドロップ領域が複数ある場合

以下の codesandbox のように、ドロップ領域が複数ある場合を見ていきます。

ドロップ領域を増やし、それぞれのエリアでのドロップ回数を計算します。

App.tsx
...省略

export default function App() {
  // ドロップカウント
- const [dropCountA, setDropCountA] = useState(0);
+ const [dropCountA, setDropCountA] = useState(0);
+ const [dropCountB, setDropCountB] = useState(0);
+ const [dropCountC, setDropCountC] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
          onDragEnd={(event) => {
            const { over } = event;
            if (over == null) {
              return;
            }
+           switch (over.id) {
+            case "dropAreaA":
+              setDropCountA((x) => x + 1);
+              break;
+            case "dropAreaB":
+              setDropCountB((x) => x + 1);
+              break;
+            case "dropAreaC":
+              setDropCountC((x) => x + 1);
            }
          }}
        >
          <Box
            sx={{
              mb: 5
            }}
          >
            <Draggable />
          </Box>
+         <Stack spacing={5} direction={"row"}>
+           <Droppable id="dropAreaA" label="ドロップエリアA">
+             {dropCountA}回ドロップしたぜ
+           </Droppable>
+           <Droppable id="dropAreaB" label="ドロップエリアB">
+             {dropCountB}回ドロップしたぜ
+           </Droppable>
+           <Droppable id="dropAreaC" label="ドロップエリアC">
+             {dropCountC}回ドロップしたぜ
+           </Droppable>
+         </Stack>
        </DndContext>
      </Box>
    </div>
  );
}

Droppableを複数個呼び出し、それぞれに固有のidを付与することで、DndContextのイベントハンドラーでoveridを見て、ドロップカウントの計算をすることができます。また、上述の「ドラッグアイテムが複数ある場合」で使用したdataプロパティはuseDroppableにも渡すことができるので、その方法でも構いません!

Collision detection

上述の実装方法で複数のドロップ領域には対応できるのですが、ドラッグアイテムがドロップしたい領域に判定が当たらず、隣の領域に当たってしまう時があります。。😢

これは、dnd kit が設定している Collision detection(衝突判定) のアルゴリズムのデフォルトがRectangle intersectionという長方形の 4 つの側面の間に隙間がないことを確認することで動作するものが設定されているからです。基本的にはこのアルゴリズムは多くのユースケースに適しているのですが、複数のドロップ領域が密集している場合などで、アイテムがどのドロップ領域に属するのかを正確に判定することが難しくなります。dnd kit はそのようなケースに対応するために複数のアルゴリズムを用意してくれています。

  • Rectangle intersection
    DndContext はデフォルトでこのアルゴリズムを使用します。
    このアルゴリズムは、長方形の 4 つの側面の間に隙間がないことを確認することで動作します。
  • Closest center
    アクティブなドラッグ可能アイテムの境界矩形の中心に最も近いドロップ可能なコンテナを検出します。
  • Closest corners
    アクティブなドラッグ可能アイテムの四隅と各ドロップ可能コンテナの四隅との間の距離を測定して、最も近いものを検出します。
  • Pointer within
    マウスポインタが他のドロップ可能コンテナの境界矩形内に含まれている場合にのみ衝突を検出します。

今回のようなケースだとマウスポインタがドロップ領域に入ったら判定されるPointer withinが最適だと思います。DndContextcollisionDetectionpointerWithinを渡します。

App.tsx
import {
  DndContext,
+ pointerWithin
} from "@dnd-kit/core";

export default function App() {
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
+         collisionDetection={pointerWithin}
        >
        ...
  );
}


collisionDetectionpointerWithinを指定することで、画像の通りマウスポインタが重なったドロップ領域に判定が当たるようになりました!pointerWithinを設定した codesandbox を用意したので、さわってみてください!

collisionDetectionを dnd kit から提供されているもの以外でも、自分のカスタマイズしたアルゴリズムを設定することもできます!

ドラッグしている要素の見た目を変える

通常、アイテムをドラッグすると、そのアイテムは元の位置から動かされます。よくあるドラッグ&ドロップでドラッグ元の要素を opacity かけて表示しておくものとかを見かけると思います。そのような挙動を再現するには DragOverlay という機能を使うと実現可能です。DragOverlayを使用すると、 実際のアイテムは元の位置に残ったまま、ドラッグ中のアイテムの「見た目」だけがカーソルに追従され、オーバーレイとして表示されます。 この機能の利点は、ドラッグ中のアイテムの見た目のカスタマイズできること、また。ドラッグが終了した時にアイテムを元の位置に戻すアニメーションなどの複雑な動作を簡単に実装することができます。以下は、実装例です。

DragOverlayでドラッグ中のスタイル用のコンポーネントを作ります。

DraggingItem.tsx
import { Box } from "@mui/material";

export const DraggingItem = () => {
  return (
    <Box
      sx={{
        bgcolor: "red",
        height: "100px",
        width: "100px",
        cursor: "grabbing",
        opacity: ".7",
        display: "flex",
        alignItems: "center",
        justifyContent: "center"
      }}
    >
      ドラッグ中
    </Box>
  );
};

作成したDraggingItemDndContext内に配置します。

App.tsx
- import { DndContext } from "@dnd-kit/core";
+ import { DndContext, DragOverlay } from "@dnd-kit/core";

...省略

export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
          onDragEnd={(event) => {
            const { over } = event;
            if (over == null) {
              return;
            }
            setDropCount((x) => x + 1);
          }}
        >
          <Box
            sx={{
              mb: 5
            }}
          >
            <Draggable id="draggableA" label="ドラッグブロック" />
          </Box>
          <Droppable id="dropAreaA">{dropCount}回ドロップしたぜ</Droppable>
+         <DragOverlay>
+           <DraggingItem />
+         </DragOverlay>
        </DndContext>
      </Box>
    </div>
  );
}

こうすることで、ドラッグ中のアイテムをオーバーレイ表示することができます!

スタイルの調整

オーバーレイは実装できたのですが、ドロップ時の挙動が少し気になります。

画像を見てもらったらわかるのですが、ドラッグを終えた時にオーバーレイ要素のDraggingItemは元の位置に戻っていき、元々のDraggableDraggingItemが元の位置に戻ってくる間は隠れます。これはDragOverlayのデフォルトの挙動でそうなっており、その指定を変更することができます。

App.tsx
import {
  DndContext,
  DragOverlay,
+ defaultDropAnimationSideEffects
} from "@dnd-kit/core";
.. 省略

export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);

  return (
    ... 省略
          <DragOverlay
+           dropAnimation={{
+             sideEffects: defaultDropAnimationSideEffects({
+               styles: {
+                 active: {
+                   background: "green"
+                 },
+                 dragOverlay: {
+                   width: "100px",
+                   height: "100px",
+                   color: "blue"
+                 }
+               }
+             }),
+             duration: 1000
+           }}
          >
            <DraggingItem />
          </DragOverlay>

    ... 省略
  );
}

一旦、指定方法をわかりやすくするために大袈裟にスタイルを当ててみています。DragOverlaydropAnimationを使用して、ドロップ時のアニメーションを指定することができます。dropAnimationsideEffectsdefaultDropAnimationSideEffectsを使い、stylesを指定します。dragOverlayが文字通り、オーバーレイのアイテムのドロップ時のstyleが指定でき、activeはドラッグ元のstyleが指定できます。また、durationeasingも指定することができます。この例ではdurationを 1s に設定しています。このように指定しますと、以下のようになります。

デフォルトだとDragOverlayをドロップした時に、activeopacity0になるので、それを打ち消し、 dragOverlayopacity0にすることで ドロップした後の挙動の違和感がなくなります。

App.tsx

export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);

  return (
    ... 省略
          <DragOverlay
+           dropAnimation={{
+             sideEffects: defaultDropAnimationSideEffects({
+               styles: {
+                 active: {},
+                 dragOverlay: {
+                   opacity: "0"
+                 }
+               }
+             })
+           }}
           >
            <DraggingItem />
          </DragOverlay>

    ... 省略
  );
}

以下、比較画像です。

デフォルト 変更後

実装例も貼っておくので、触ってみたり、値を変更してみてください。

その他のよく使いそうな設定項目

上述した以外で dnd kit で指定できる設定項目を書いていきます。

Modifiers

この props は、移動座標を動的に変更するための機能です。こちらも dnd kit が提供しているmodifiesが 6 個あります。

  • restrictToHorizontalAxis
    水平軸のみに動きを制限
  • restrictToVerticalAxis
    垂直軸のみに動きを制限
  • restrictToWindowEdges
    ドラッグ可能なアイテムがウィンドウ(ブラウザのビューポート)の端に到達したときに、それ以上進まないように制限
  • restrictToParentElement
    ドラッグ可能なアイテムの親要素内での動きを制限
  • restrictToFirstScrollableAncestor
    ドラッグ可能なアイテムの最初のスクロール可能な祖先内での動きを制限
  • createSnapModifier
    与えられたグリッドサイズにスナップする修飾子を作成する関数

コードは以下のようになります。

import { DndContext } from "@dnd-kit/core";
+ import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";

...省略

export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
+         modifiers={[restrictToHorizontalAxis]}
          onDragEnd={(event) => {
            const { over } = event;
            if (over == null) {
              return;
            }
            setDropCount((x) => x + 1);
          }}
        >
      </DndContext>

      ... 省略

autoScroll

自動スクロールを一時的または恒久的に無効にするために使用するものです。

App.tsx
export default function App() {
  // ドロップカウント
  const [dropCount, setDropCount] = useState(0);
  return (
    <div className="App">
      <Box
        sx={{
          p: 2
        }}
      >
        <DndContext
+         autoScroll
          onDragEnd={(event) => {
            const { over } = event;
            if (over == null) {
              return;
            }
            setDropCount((x) => x + 1);
          }}
        >
      </DndContext>

      ... 省略

これらの設定の動きは以下の codesandbox で色々設定できるようにしたので触って確かめてみてください!

終わりに

以上が、dnd kit の基本的な使い方でした。自分も触ってみてカスタマイズ性の高さに驚きました。また、アクセシビリティもしっかり配慮されており素晴らしいライブラリだと思いました!記事に書いたプロパティ以外にも設定できる項目が dnd kit は多数存在するので、公式ドキュメントを見てみてください!
弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion