UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【dnd-kit】カスタム collisionDetection で操作感を最適化する

dnd-kit では、ドラッグ中にどの要素を「ドロップ対象」とするかを判定する仕組みとして collisionDetection が用意されている

docs.dndkit.com

デフォルトの戦略だけでは操作感に課題が出ることも多いため、状況に応じてカスタマイズすることで最適な動作が実現できる

collisionDetection の基本

collisionDetection は、ドラッグ中に「どのドロップエリアが現在の対象か」を決定するロジック

DndContext に渡すことで動作を切り替えられる

主な戦略

戦略名 説明
closestCenter 要素の中心点同士の距離で判定(デフォルト)
pointerWithin ポインタが領域内に入っているかで判定
rectIntersection 矩形の重なり具合で判定
closestCorners 四隅の距離で判定

操作感が悪くなるケース

  • 高さや幅がバラバラな要素を扱う場合、closestCenter だと意図した判定がされない
  • 小さい要素にドロップしづらい
  • グリッドレイアウトでは、標準戦略では違和感が出やすい

このような場合は、rectIntersection やカスタム戦略が有効になる

rectIntersection の活用

rectIntersection は、ドラッグ対象の矩形とドロップ対象の矩形がどれだけ重なっているかで判定する

要素サイズに依存せず、重なった時点でドロップ判定されるため、操作感が自然になる場面が多い

使用例

import { DndContext, rectIntersection } from '@dnd-kit/core';

<DndContext collisionDetection={rectIntersection}>
  ...
</DndContext>

カスタム戦略の実装

標準の戦略では対応できない場合、自分で判定ロジックを定義できる

たとえば「重なり量が多い順に優先する」戦略を作成することが可能

例:カスタム rectIntersection

type Rect = {
  top: number;
  bottom: number;
  left: number;
  right: number;
  width: number;
  height: number;
};

function calculateIntersectionArea(a: Rect, b: Rect) {
  const xOverlap = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
  const yOverlap = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
  return xOverlap * yOverlap;
}

const customCollisionDetection: CollisionDetection = ({ droppableRects, collisionRect }) => {
  const collisions = [];

  for (const [id, rect] of droppableRects.entries()) {
    if (!rect) return;

    const intersectionArea = calculateIntersectionArea(collisionRect, rect);
    if (intersectionArea > 0) {
        collisions.push({
          id,
          data: { value: intersection },
        });
    }
  }

  // intersection率が高い順に並べて返す
  return collisions.sort((a, b) => b.data.value - a.data.value);
};

複数戦略の組み合わせ

状況に応じて、複数の戦略を切り替えることもできる。

例:pointerWithin を優先しつつ、fallback で closestCenter を使用

const combinedStrategy = (args) => {
  const pointerHits = pointerWithin(args);
  if (pointerHits.length > 0) return pointerHits;

  return closestCenter(args);
};